#+TITLE: Getting Started with Downstroke #+AUTHOR: Downstroke Project * Welcome Downstroke is a 2D tile-driven game engine for CHICKEN Scheme, built on SDL2. Its API is inspired by Phaser 2: a minimal game is about twenty lines of Scheme, with the engine taking care of window creation, the main loop, input, and rendering. This guide walks you through building your very first Downstroke game — a blue square you can push around the screen with the arrow keys. By the end you will have the complete source of the =getting-started= demo and a clear map of where to go next. You write a Downstroke game by calling ~make-game~ with a few keyword arguments (a title, a size, and one or two lifecycle hooks), and then handing the result to ~game-run!~. Everything else — opening the window, polling input, clearing the framebuffer, presenting the frame — is the engine's job. You only describe /what/ the game is: what the scene looks like, what the entities are, and how they change. * The minimum you need The smallest Downstroke program you can write opens a window, runs the main loop, and quits when you press =Escape=. No scene, no entities, no update logic — just the lifecycle shell: #+begin_src scheme (import scheme (chicken base) downstroke-engine) (game-run! (make-game title: "Hello Downstroke" width: 320 height: 240)) #+end_src Save that as =hello.scm=, compile, and run. You should get a 320×240 window with a black background. Press =Escape= to quit. Two things are worth noticing: - ~make-game~ takes /keyword arguments/ (note the trailing colon: ~title:~, ~width:~, ~height:~). They all have defaults, so you can leave any of them off. The window defaults to 640×480 and titled "Downstroke Game". - ~game-run!~ is what actually starts SDL2 and enters the main loop. It blocks until the player quits. There are four lifecycle hooks you can attach to ~make-game~: | Keyword | When it runs | Signature | |------------+-------------------------------+--------------------------| | ~preload:~ | once, before ~create:~ | ~(lambda (game) ...)~ | | ~create:~ | once, after ~preload:~ | ~(lambda (game) ...)~ | | ~update:~ | every frame | ~(lambda (game dt) ...)~ | | ~render:~ | every frame, after scene draw | ~(lambda (game) ...)~ | In the rest of this guide you will fill in ~create:~ (to build the scene) and ~update:~ (to move the player). * Core concepts ** Creating a scene A /scene/ is the container for everything the engine draws and simulates: a list of entities, an optional tilemap, a camera, and a background color. You build one with ~make-scene~ or — for sprite-only games like ours — with the simpler ~make-sprite-scene~, which skips the tilemap fields. In the ~create:~ hook you typically build a scene and hand it to the game with ~game-scene-set!~: #+begin_src scheme (import scheme (chicken base) downstroke-engine downstroke-world downstroke-scene-loader) (game-run! (make-game title: "Blue Window" width: 320 height: 240 create: (lambda (game) (game-scene-set! game (make-sprite-scene entities: '() background: '(20 22 30)))))) #+end_src The ~background:~ argument is a list of three or four integers ~(r g b)~ or ~(r g b a)~ in the 0–255 range. The engine clears the framebuffer with this color at the top of every frame. If you omit it, the background is plain black. ~make-sprite-scene~ also accepts ~camera:~, ~camera-target:~, ~tileset:~, ~tileset-texture:~, and ~engine-update:~, but you do not need any of them for the guide. ** Adding an entity In Downstroke an /entity/ is just an alist — a list of ~(key . value)~ pairs — with a small set of conventional keyword keys. There are no classes and no inheritance; an entity is data you can read with ~entity-ref~ and transform with ~entity-set~. It is common to write entities in plist form (alternating keys and values) for readability and then convert them with ~plist->alist~ from the =list-utils= egg: #+begin_src scheme (import (only (list-utils alist) plist->alist)) (define (make-player) (plist->alist (list #:type 'player #:x 150 #:y 100 #:width 32 #:height 32 #:color '(100 160 255)))) #+end_src The keys in use here are the conventional ones the engine understands: - ~#:type~ — a symbol you use to distinguish entities; purely for your own bookkeeping. - ~#:x~, ~#:y~ — pixel position of the entity's top-left corner. - ~#:width~, ~#:height~ — pixel size of the entity's bounding box. - ~#:color~ — an ~(r g b)~ or ~(r g b a)~ list. When an entity has no ~#:tile-id~ (or the scene has no tileset texture), the renderer fills the entity's rectangle with this color. That is exactly what we want for our guide: no sprite sheet, just a colored box. Once you have a player factory, drop one into the scene's ~entities:~ list: #+begin_src scheme (make-sprite-scene entities: (list (make-player)) background: '(20 22 30)) #+end_src Compile and run, and you should see a light-blue 32×32 square on a dark background. It does not move yet — that is the next step. ** Reading input Input in Downstroke is organised around /actions/ rather than raw keys. The engine ships with a default input config (~*default-input-config*~ in ~downstroke-input~) that maps the arrow keys, WASD, a couple of action buttons, and game-controller buttons to a small set of action symbols: | Action | Default keys | |----------+-------------------| | ~up~ | =Up=, =W= | | ~down~ | =Down=, =S= | | ~left~ | =Left=, =A= | | ~right~ | =Right=, =D= | | ~a~ | =J=, =Z= | | ~b~ | =K=, =X= | | ~start~ | =Return= | | ~select~ | (controller only) | | ~quit~ | =Escape= | Inside ~update:~ you reach the input state with ~(game-input game)~, and then query it with three predicates from ~downstroke-input~: - ~(input-held? input 'left)~ — ~#t~ while the player holds the action down. - ~(input-pressed? input 'a)~ — ~#t~ only on the first frame the action goes down (edge). - ~(input-released? input 'a)~ — ~#t~ only on the frame the action goes up. ~input-held?~ is what we want for continuous movement: #+begin_src scheme (define +speed+ 2) (define (input-dx input) (cond ((input-held? input 'left) (- +speed+)) ((input-held? input 'right) +speed+) (else 0))) (define (input-dy input) (cond ((input-held? input 'up) (- +speed+)) ((input-held? input 'down) +speed+) (else 0))) #+end_src ~+speed+~ is pixels per frame. At the engine's default 16 ms frame delay that works out to roughly 120 pixels per second; feel free to adjust. The =quit= action is already wired into the main loop: pressing =Escape= ends the game, so you do not need to handle it yourself. ** Updating entity state Entities are immutable. ~entity-set~ does not mutate; it returns a /new/ alist with the key updated. To move the player, read its current position, compute the new position, and produce a new entity: #+begin_src scheme (define (move-player player input) (let* ((x (entity-ref player #:x 0)) (y (entity-ref player #:y 0)) (dx (input-dx input)) (dy (input-dy input))) (entity-set (entity-set player #:x (+ x dx)) #:y (+ y dy)))) #+end_src ~entity-ref~ takes an optional default (~0~ above), returned if the key is absent. Once you have a new entity, you also need a new scene that contains it, because scenes are immutable too. ~update-scene~ is the copier: pass it an existing scene and any fields you want to change. #+begin_src scheme (update-scene scene entities: (list (move-player player input))) #+end_src Finally, install the new scene on the game with ~game-scene-set!~. Putting it all together in the ~update:~ hook: #+begin_src scheme update: (lambda (game dt) (let* ((scene (game-scene game)) (input (game-input game)) (player (car (scene-entities scene)))) (game-scene-set! game (update-scene scene entities: (list (move-player player input)))))) #+end_src A note about ~dt~: the update hook is called with the number of milliseconds elapsed since the previous frame. We ignore it here because we move by a fixed number of pixels per frame, but most real games use ~dt~ to make motion frame-rate independent — see [[file:physics.org][physics.org]] for the engine's built-in pipeline which does this for you. * Putting it together Here is the full =demo/getting-started.scm= source. Read it top to bottom — each piece should now look familiar. #+begin_src scheme (import scheme (chicken base) (only (list-utils alist) plist->alist) downstroke-engine downstroke-world downstroke-entity downstroke-input downstroke-scene-loader) (define +speed+ 2) (define (make-player) (plist->alist (list #:type 'player #:x 150 #:y 100 #:width 32 #:height 32 #:color '(100 160 255)))) (define (move-player player input) (let* ((x (entity-ref player #:x 0)) (y (entity-ref player #:y 0)) (dx (cond ((input-held? input 'left) (- +speed+)) ((input-held? input 'right) +speed+) (else 0))) (dy (cond ((input-held? input 'up) (- +speed+)) ((input-held? input 'down) +speed+) (else 0)))) (entity-set (entity-set player #:x (+ x dx)) #:y (+ y dy)))) (game-run! (make-game title: "Getting Started" width: 320 height: 240 create: (lambda (game) (game-scene-set! game (make-sprite-scene entities: (list (make-player)) background: '(20 22 30)))) update: (lambda (game dt) (let* ((scene (game-scene game)) (input (game-input game)) (player (car (scene-entities scene)))) (game-scene-set! game (update-scene scene entities: (list (move-player player input)))))))) #+end_src This file ships with the engine. To run it, build the project and launch the demo binary: *Getting started demo* — run with =bin/demo-getting-started=, source in =demo/getting-started.scm=. From the Downstroke source tree, ~make~ will compile the engine and ~make demos~ will build every demo executable under =bin/=. When the binary starts you should see a 320×240 window with a blue square you can push around with the arrow keys (or =WASD=). =Escape= quits. A few things to notice in the final code: - We never mutate anything. ~move-player~ returns a new entity; ~update-scene~ returns a new scene; ~game-scene-set!~ swaps the scene on the ~game~ struct for the next frame. - Every frame, ~update:~ rebuilds the entire entities list from scratch. That is fine for a one-entity demo and scales happily into the dozens; for larger games the engine's built-in physics pipeline (see [[file:physics.org][physics.org]]) does the same work for you using keyword conventions like ~#:vx~ and ~#:vy~. - The engine's /default engine update/ — the physics pipeline — still runs before your ~update:~ hook. It is a no-op for our player because we never set velocity keys like ~#:vx~ or ~#:vy~, so the only thing moving the square is your own ~move-player~. The moment you set ~#:vx~ to a non-zero number, the engine will start applying it for you. * Where to next Once =getting-started= runs, the rest of Downstroke is a menu of subsystems you can opt into piece by piece. Every topic below has its own doc file and at least one matching demo: - [[file:entities.org][entities.org]] — the entity model, the list of conventional keys, and how to build reusable prefabs. - [[file:physics.org][physics.org]] — the built-in frame pipeline: gravity, velocity, tile collisions, ground detection, and entity-vs-entity collisions. - [[file:scenes.org][scenes.org]] — scenes, cameras, camera follow, and switching between named game states. - [[file:rendering.org][rendering.org]] — how the renderer draws sprites, solid-color rects, and bitmap sprite fonts. - [[file:animation.org][animation.org]] — frame-based sprite animation driven by ~#:animations~ and ~#:anim-name~. - [[file:input.org][input.org]] — customising the action list, keyboard map, and game-controller bindings. - [[file:tweens.org][tweens.org]] — declarative easing for position, scale, color, and other numeric keys. - [[file:audio.org][audio.org]] — loading and playing sound effects and music. And the full set of demos that ship with the repository, each built by ~make demos~: - [[file:../demo/getting-started.scm][Getting started]] (=bin/demo-getting-started=) — arrow keys move a blue square; the starting point for this guide. - [[file:../demo/platformer.scm][Platformer]] (=bin/demo-platformer=) — gravity, jumping, tile collisions, frame animation. - [[file:../demo/shmup.scm][Shmup]] (=bin/demo-shmup=) — top-down shooter-style movement and firing. - [[file:../demo/topdown.scm][Topdown]] (=bin/demo-topdown=) — four-direction movement without gravity. - [[file:../demo/audio.scm][Audio]] (=bin/demo-audio=) — background music and sound effects. - [[file:../demo/sandbox.scm][Sandbox]] (=bin/demo-sandbox=) — group prefabs composed from mixins. - [[file:../demo/spritefont.scm][Sprite font]] (=bin/demo-spritefont=) — bitmap font rendering. - [[file:../demo/menu.scm][Menu]] (=bin/demo-menu=) — a simple UI menu. - [[file:../demo/tweens.scm][Tweens]] (=bin/demo-tweens=) — side-by-side comparison of easing functions. - [[file:../demo/scaling.scm][Scaling]] (=bin/demo-scaling=) — logical-resolution scaling via ~scale:~. - [[file:../demo/animation.scm][Animation]] (=bin/demo-animation=) — frame-based sprite animation. Pick the demo closest to the game you want to build, open its source next to the matching topic doc, and you will have a concrete example of every API call in context.