`ludic build` produces a program. Double-clicked it opens a Terminal window, it wears the generic executable icon, it calls itself whatever the file is called, and it carries none of its assets. `ludic bundle` produces an application. Everything it needs is in package.ludic, so the command takes no arguments: an Info.plist and PkgInfo from `app` lines, an .icns built by sips and iconutil at all ten sizes macOS asks for from a single source PNG, the asset pack in Contents/Resources, and an ad-hoc signature - which is not optional on Apple silicon, where an unsigned binary is killed rather than warned about. The bundle identifier falls back to the package path reversed, so a project that never thinks about it still gets a defensible one instead of two apps sharing a key Launch Services hangs the Dock, saved state and permissions off. A bundled game is moved to ~/Library/Application Support/<name> before main, because Finder starts a .app with its working directory at "/" where no save could ever be written. Reads come out of the pack, writes land somewhere real and per-user, and the game's save code needs no change and no platform knowledge. The splash is the other half of looking like an application. A game that loads 165 MB spends a visible moment doing it with nothing on screen, which from the outside is indistinguishable from a launch that failed. splash_show puts a borderless window up from the same constructor that mounts the pack - before main, so it appears while the process is still starting rather than after the slow part it exists to cover - and reads the artwork out of the pack like any other asset. It turns the run loop enough times to be mapped and composited there and then; once composited the backing store survives a busy main thread, so it stays up for the whole load. Nothing hides it automatically. Only the game knows when its first real frame is ready, and a splash that vanishes before that leaves the same black gap it was covering, so the game calls App.splash_hide(). Headless there is no splash and the call lowers to nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
199 lines
8.7 KiB
Markdown
199 lines
8.7 KiB
Markdown
# Ludic
|
|
|
|
A compiled language for 2D games. The entity-component system is part of the
|
|
syntax, the runtime is deterministic fixed-point, and `ludicc` lowers Ludic
|
|
straight to LLVM IR — **no C is generated, compiled or linked in a build.**
|
|
|
|
The compiler is written in Ludic. It compiles its own source to a byte-exact
|
|
fixpoint and rebuilds from a checked-in IR seed with clang alone; CI asserts
|
|
that on every push.
|
|
|
|
- **Documentation:** <https://workshopsoft.pages.workshopsoft.io/ludic/>
|
|
- **API reference:** <https://workshopsoft.pages.workshopsoft.io/ludic/api.html>
|
|
- **Issues:** <https://git.workshopsoft.io/workshopsoft/ludic/issues>
|
|
|
|
```ludic
|
|
program Hello {
|
|
|
|
property Position { column: int = 0, row: int = 0 }
|
|
property Velocity { delta_x: int = 0, delta_y: int = 0 }
|
|
|
|
handler SpawnEnemies phase Start {
|
|
spawn Enemy { Position { column: 3, row: 4 }, Velocity { delta_x: 1, delta_y: 0 } }
|
|
spawn Enemy { Position { column: 10, row: 2 }, Velocity { delta_x: 0, delta_y: 1 } }
|
|
}
|
|
|
|
# a handler declares the entities it touches; the body runs
|
|
# once per match, with each property bound by name.
|
|
@Queries(these: [Position, Velocity])
|
|
handler AdvancePositions phase FixedUpdate {
|
|
Position.column += Velocity.delta_x
|
|
Position.row += Velocity.delta_y
|
|
}
|
|
}
|
|
```
|
|
|
|
## Getting started
|
|
|
|
Install the toolchain — the compiler, the `ludic` CLI, the engine runtime, the
|
|
formatter and the language server — with one command:
|
|
|
|
```bash
|
|
curl -fsSL https://workshopsoft.pages.workshopsoft.io/ludic/install.sh | sh
|
|
```
|
|
|
|
It installs into `~/.ludic` and puts `~/.ludic/bin` on your `PATH` in every
|
|
shell — the PATH line lives in `~/.ludic/env`, sourced from `~/.profile`,
|
|
`~/.zshenv` and your bash or fish config. Nothing else on the machine is touched;
|
|
uninstalling is `rm -rf ~/.ludic` and deleting those two-line blocks. Where a
|
|
prebuilt toolchain exists for your platform it is downloaded and verified against
|
|
a published checksum; where it does not, the installer bootstraps from the
|
|
compiler's own IR seed with clang. Either way you need clang (or Xcode's Command
|
|
Line Tools) to link, since Ludic emits LLVM IR and links it natively.
|
|
|
|
Then make a game:
|
|
|
|
```bash
|
|
ludic new mygame
|
|
cd mygame
|
|
ludic run # compiles src/main.ludic and opens a native window
|
|
```
|
|
|
|
`ludic new` writes a manifest, a program that already moves something on screen,
|
|
and a test. `ludic build` stops at the binary; `ludic bundle` goes on to the
|
|
thing you can actually give someone. Rendering is deterministic, so a frame can
|
|
be produced without a window, which is what CI diffs:
|
|
|
|
```bash
|
|
ludic test
|
|
ludic build --headless
|
|
printf 'ddddwww' | ./build/mygame_headless # writes build/out.ppm
|
|
```
|
|
|
|
`ludic help` lists every command, and `ludic doctor` checks the install.
|
|
[`examples/`](examples/README.md) is a tour grouped by intent: games, rendering,
|
|
ECS, events, networking, language features and the standard library — compile any
|
|
of them with `ludic build examples/games/snake.ludic`.
|
|
|
|
### Building from a checkout
|
|
|
|
Contributors also get `ludic-dev`, a second binary carrying the toolchain's own
|
|
tasks — building the compiler, the suites, the docs site, releases. It is built
|
|
from a checkout and is not part of an install, so nothing a user runs is mixed
|
|
up with it. Bootstrapping is the only step Ludic cannot do for itself, since
|
|
compiling Ludic needs a compiler — clang assembles the checked-in IR seed, and
|
|
that compiler builds the rest:
|
|
|
|
```bash
|
|
mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc
|
|
bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev
|
|
bin/ludic-dev build # -> bin/{ludicc,ludic,ludic-dev,ludic-fmt,ludic-lsp}
|
|
bin/ludic-dev test # the regression suite
|
|
```
|
|
|
|
## The language
|
|
|
|
- **ECS in the syntax.** `property`, `model` and `handler` are keywords. Query
|
|
with `for (a, b) in query [A, B, {Tag}] where <expr> { … }`; `spawn` and
|
|
`despawn` recycle entity slots; `@`-annotations drive lifecycle hooks.
|
|
- **Deterministic by construction.** Q16.16 `fixed` arithmetic and a seeded RNG
|
|
give the same frame byte-for-byte on every run — the basis for replays,
|
|
lockstep netcode and golden-image tests.
|
|
- **Scenes and state machines.** `scene` / `layer` / `become` model
|
|
mutually-exclusive game states with enter and exit hooks; `match` / `machine`
|
|
/ `state` handle dispatch and per-entity FSMs.
|
|
- **Events and networking.** A cancellable event bus (`event` / `emit` / `@On`)
|
|
and networking primitives (`@Sync`, ownership, RPCs) over a built-in transport.
|
|
- **Batteries in the language.** Framebuffer primitives, PNG sprites, TrueType
|
|
text and a retained `ui` widget tree declared as data, plus a namespaced
|
|
standard library (`Math`, `Text`, `List`, `Random`, `Crypto`, `Tiled`, …).
|
|
- **Whole-world snapshots.** `save()` and `load()` serialize every entity,
|
|
property and program `var` in one call.
|
|
|
|
[LANGUAGE.md](LANGUAGE.md) is the full reference; the
|
|
[API reference](https://workshopsoft.pages.workshopsoft.io/ludic/api.html)
|
|
documents every symbol on its own page.
|
|
|
|
## Shipping
|
|
|
|
A built binary is a program, not an application: it opens its assets by a path
|
|
relative to the working directory, so it runs from the project root and nowhere
|
|
else, and it wears the generic executable icon.
|
|
|
|
```bash
|
|
ludic pack # every asset the game opens, into one .lpak
|
|
ludic bundle # ...and that, the binary, an icon and the metadata, as a .app
|
|
```
|
|
|
|
Nothing about how the game is written changes. `gltf_load("assets/kit/hiker",
|
|
…)` reads a file during development and a run of bytes inside the bundle once
|
|
shipped, and cannot tell which — the pack is spliced in at `file_open`, the one
|
|
place every asset in a Ludic program comes through. A bundled game also gets a
|
|
boot splash it controls (`App.splash_hide()`) and a writable home under
|
|
Application Support, because Finder starts a `.app` at `/` where no save could
|
|
be written.
|
|
|
|
Without a pack beside it — which is every `ludic run` — nothing mounts and every
|
|
open goes to the filesystem exactly as before. See [docs/SHIPPING.md](docs/SHIPPING.md).
|
|
|
|
## Packages
|
|
|
|
Dependencies are identified by URL, resolved with minimal version selection, and
|
|
cached in a content-addressed store:
|
|
|
|
```bash
|
|
ludic add git.workshopsoft.io/user/pkg # resolve, fetch, link into ludic_modules/
|
|
ludic get # install from package.ludic, write the lock
|
|
ludic verify # check locked packages against the store
|
|
```
|
|
|
|
The `ludic.*` packages — canonical ECS components, the gameplay, platformer,
|
|
RPG, shooter and NPC-AI modules — ship with the toolchain, so importing one needs
|
|
no fetch step at all.
|
|
|
|
See [`docs/PACKAGES.md`](docs/PACKAGES.md) for the manifest and lockfile model.
|
|
|
|
## Editor support
|
|
|
|
Editors spawn `ludic lsp`; the server ships with the toolchain, so there is
|
|
nothing extra to install. It speaks LSP 3.17 over stdio, so one binary serves
|
|
every editor: completion, diagnostics from the compiler itself, go-to-definition
|
|
and rename across imports, and comment-preserving formatting. `ludic fmt` runs
|
|
the same formatter as a CLI, for pre-commit hooks. Both understand
|
|
```` ```ludic ```` fences in Markdown. Plugins and drop-in config for VS Code, JetBrains, Neovim,
|
|
Helix, Emacs, Sublime and Zed are in [`tools/editors/`](tools/editors/README.md).
|
|
|
|
## Status
|
|
|
|
The native 2D backend ships: a Cocoa window on macOS, a headless renderer for
|
|
CI, and the whole runtime — framebuffer, PNG/DEFLATE decoding, TrueType
|
|
rasterizer, retained UI, RNG — written in Ludic under
|
|
[`runtime/native/`](runtime/native/). Only the window seam (`win_*`: window,
|
|
keys, mouse, cursor, gamepad, touch) is hand-written LLVM IR against the
|
|
platform ABI, the same floor Rust and Swift stand on.
|
|
|
|
The **web/wasm32 backend is not currently available.** The browser platform
|
|
layer is in-tree under [`runtime/web/`](runtime/web/), but emitting wasm was a
|
|
capability of the retired C compiler and has not been re-wired on the
|
|
self-hosted toolchain. `--target` cross-compilation and `--shared` libraries are
|
|
in the same position. See [COMPILING.md](COMPILING.md).
|
|
|
|
Releases follow SemVer and are cut from changesets by `ludic-dev release`, then built
|
|
and published by CI from the tag; see [CHANGELOG.md](CHANGELOG.md).
|
|
|
|
## Contributing
|
|
|
|
[CONTRIBUTING.md](CONTRIBUTING.md) covers the development loop, the commit and
|
|
code conventions, how the bootstrap fixpoint works, and what a self-hosted CI
|
|
runner needs. Issue and pull-request templates are under
|
|
[`.forgejo/`](.forgejo/).
|
|
|
|
## License
|
|
|
|
The compiler and runtime are licensed under the
|
|
[Apache License 2.0](LICENSE) (`SPDX-License-Identifier: Apache-2.0`).
|
|
|
|
The bundled [Kenney](https://kenney.nl) art under `assets/kenney/` is
|
|
third-party and released under **CC0 1.0**; each pack keeps its own
|
|
`License.txt`. Code and assets are licensed separately — Apache-2.0 covers the
|
|
source, not the art.
|