Tutorial #4. SDL Demo

Step 1. Create a separate module for the graphical scene. Let's name this module gui.

using sys { Array }  // Import `Array` class from `sys` module
using array;         // Import various extensions to the array class
using sdl { Window } // Import `Window` class from the `sdl` module 

//
// All scene items will implement the actor interface.
//
interface Actor {

    // Called to redraw window.
    // Should draw its appearance on the `dst` window.
    paint(dst Window);

    // Called on every frame, get milliseconds since last `tick` as a parameter.
    // If returns false, its container deletes it.
    tick(milliseconds int) bool;
}

// Group of actors
class Group {
    +Array(Actor);  // Inherits `Array` parameterized with `Actor`s
    +Actor{         // Implements interface `Actor`

        // Interface implementation (or base class overloads) always
        // go in its own {}-scope, to avoid name conflicts between
        // newly added methods and/or methods from different bases.
        paint(dst Window) {

            // `each` is the `Array` extension method. It executes its lambda for
            // every existing element in the array.
            each{ _.paint(dst) }
        }
        tick(milliseconds int) bool {

            // `plow` is another Array extension method. See: array.ag.
            // It executes lambda for each array element and removes element
            // if lambda returns `false`
            plow{ _.tick(milliseconds) };

            // If group is nested in the another group, it automatically deletes
            // if it gets empty.
            size() != 0
        }
    }
}

Step 2. Create a main application module.

using sdl { Window, Sdl, Texture }
using utils { Random, forRange }
using gui { Actor, Group }        // Import our newly created module

Step3. Entry point

Scene.init()?_.run();

This line of code must always be the last line of the source file. What it does:

  • Creates an instance of the demo Scene class (not yet implemented).
  • Calls its init method, that should return the ?Scene pointer (optional temporary pointer to Scene):
    • nothing - if initialization is failed
    • or this if it's succeeded.
  • Checks the result of initialization, and if it's not nothing, calls its run method.

Step 4. Implement Scene

class Scene{
    +Group;   // Our `Scene` extends `gui_Group`

    init() ?Scene {  // So far its `init` and `run` methods are empty
       this
    }
    run() {
    }
}

Step 5. Initialize Sdl, window and run event loop

class Scene{
    ...
    sdl = Sdl;   // Scene will own an SDL library instance
    w = Window;  // ...And a Window instance

    init() ?Scene {
        sdl.initVideo() &&           // If sdl initialized successfully
        w.createCentered(            // and our window managed to create...
            "Hello from Argentum!",
            800, 600)
        ? this                       // ... then return `this`.
    }
    run() {
        sdl.eventLoop `e {           // Runs event loop till event `quit`.
                                     // Lambda is called every 1/30 second.
             paint(w);       // Call `gui_Group.paint` method to draw on back buffer.
             w.flip();       // Make back buffer appeared on screen.
             tick(1000/30);  // Call `gui_Group.tick` method to animate.
        };
    }
}

Instead of using 800 and 600 we could define and use named constants: const xWidth=800;

Now we can run our application and see an empty window:

Step 6. Load and uphold graphic resources:

class Scene{
    ...
    flare = Texture;       // Scene will own all textures
    star = Texture;
    background = Texture;
    foreground = Texture;

    init() ?Scene {
        sdl.initVideo() && w.createCentered(...) &&
        background.init(w, "back.jpg") &&            // Load all images
        foreground.init(w, "fore.png") &&
        flare.init(w, "round.png") &&
        star.init(w, "star.png")
            ? this
    }
    run() {
        sdl.eventLoop `e {
            w.blt(background, 0, 0, 800, 600, 0, 0, 800, 600); // Draw back...
            paint(w);
            w.blt(foreground, 0, 0, 800, 600, 0, 0, 800, 600); // and foreground
            ...
        };
    }
}

Now our application gets more interesting:

Step 7. Add Flare actor

class Scene{
    ...
    rnd = Random;     // Create and store in our scene a random number generator
    ...
    run() {
        push(10, Flare.initAtBottom(this));   // In the beginning, add 10 Flares to the
                                              // scene . Push is the Array extension method.
        sdl.eventLoop `e {
            ...
            rnd.get(0, (size() / 2) + 1) == 0 ?      // Depending of the number of existing
                  append(Flare.initAtBottom(this));  // scene items, add more flares.
        };
    }
}

class Flare {
    +Actor {
        paint(dst Window) {
            sprite? {                        // If sprite object exists
                _.setColorMod(color);        // ...made it of our color,
                _.setAlphaMod(fade >> 8);    // ..set our transparency
                size = fade >> 8 >> 2;       // ..and draw it with size
                dst.blt(_, 0, 0, 64, 64, x >> 16 - size/2, y >> 16 - size/2, size, size);
            }
        }
        tick(milliseconds int) bool {
            x += dx;              // Movement
            y += dy;
            dy += 1 << 13;        // Gravity
            fade += dFade;
            dFade -= 140;         // Fade-out speed
            // Remove it from the scene if it's outside screen or had faded out.
            y > -100 << 16 && y <= 600 << 16 && x > 0 && x < 800 << 16 && fade >= 0
        }
    }
    initAtBottom(scene Scene) this {  // Initializer method
        r = scene.rnd;                               // Acquire scene's random generator
        x := r.get(200 << 16, (800-200) << 16);      // and use it for coordinates
        dx := r.get(-x / 30, (800 << 16 - x) / 30);  // and speed
        dy := r.get(-18 << 16, -8 << 16);
        sprite := &scene.flare;       // Store association to the sprite that lives in the scene
        color := makeBrightColor(r);  // Call a global function
    }
    x = 0;              // Flare fields
    dx = 0;
    fade = 0;
    dFade = 4200;
    y = 600 << 16;
    dy = -10 << 16;
    color = 0xff_ff_ff;
    sprite = &Texture;  // Unbounded &-pointer to the sdl_Texture.
}

// Standalone function.
// Make one of 8-bit-styled colors with the given `rnd` generator.
fn makeBrightColor(rnd Random) int {
    r = (rnd.get(-1, 1) & 0xff) |
        (rnd.get(-1, 1) & 0xff << 8) |
        (rnd.get(-1, 1) & 0xff << 16);
    r == 0 ? 0x0ffffff : r             // B/c we have black background
}

Now our application shows multiple flying flares of different colors, opacities and sizes:

Step 8. Add sparkles (fill our scene with polymorphism)

Add a Sparkle class, and make flares spawn sparkles:

class Sparkles {
    scene = &Scene;  // Non-owning reference to the `Scene`
    x = 0;
    y = 0;
    r = 30;      // Used for time-to-live, density and size
    color = 0;
    +Actor {
        paint(dst Window) {
            // If scene exists, use its random numbers.
            // distributeInCircle - is a stand-alone function.
            scene? distributeInCircle(_.rnd, r, r) x y {
                dst.fillRect(this.x + x, this.y + y, 1, 1, color);
            }
        }
        tick(milliseconds int) bool { (r -= 1) > 0 }
    }
    init(scene Scene, x int, y int, color int) this {
        this.scene := &scene;
        this.x := x;
        this.y := y;
        this.color := color;
    }
}

// Make flares know their scene
class Flare {
    scene = &Scene;  // non-owning &-pointer
    +Actor {
        ...
        tick(milliseconds int) bool {
            ...
            // If scene exists, maybe add a sparkle to it.
            // ?`name allows to define own name instead of `_` 
            scene ?`sc sc.rnd.get(0, 2) == 0 ?
                sc.append(Sparkles.init(sc, x >> 16, y >> 16, color));
            ...
        }
    }
    initAtBottom(scene Scene) this {
        this.scene := &scene;   // Store a reference to our scene
        ...
    }
}

// A stand-alone functions that:
// calls no more than `count` times its `callback` with coordinates
// randomly distributed in a circle of radius `r` using `rnd` renerator.
fn distributeInCircle(rnd Random, count int, r int, callback (int, int)) {
    forRange(0, count) `i {
        x = rnd.get(-r, r);
        y = rnd.get(-r, r);
        x*x + y*y < r*r ? callback(x, y);
    }
}

Now our flares leave tails of fading sparkles:

Step 9. Make our flares explode with more flares:

class Flare {
    +Actor {
        tick(milliseconds int) bool {
            ...
            // In the end of the flare life cycle depending on the generation
            // maybe add 6..15 more flares with speeds distributed in circle.
            fade == 0 && scene &&=s s.rnd.get(0, generation) == 0 ?
                distributeInCircle(s.rnd, s.rnd.get(6, 15), 10 << 16) dx dy {
                    s.append(Flare.initAtFlare(this, dx, dy));
                };
            ...
        }
    }
    // Another named initialization method.
    initAtFlare(parent Flare, dx int, dy int) this {
        scene := parent.scene;
        scene? sprite := &_.star;    // Take a different sprite from the scene
        x := parent.x;
        y := parent.y;
        color := parent.color;
        generation := parent.generation + 1;
        this.dx := dx;
        this.dy := dy;
    }
    ...
    generation = 1;
}

This gives us secondary star-shaped flares:

The full source of demo.ag can be found in the Windows demo source file directory: "src/demo.ag".

This demo covers:

  • Modules
  • Tree-like structures.
  • Interfaces.
  • Initialization methods.
  • Base classes/interfaces scope.
  • &-references.
  • ?=name and &&=name control flow constructions.
  • Stand-alone functions.
  • Lambdas.
  • SDL integration.

Leave a Reply

Your email address will not be published. Required fields are marked *