Möbius Runner

sign up sign in

Hi everyone, I recently made this game (which you can play online) for a small game jam I did with some friends. It's a möbius-strip version of an infinite runner game. I'm going to walk through a few interesting ways I used luxe in this project, though keep in mind that it's not a step-by-step tutorial for recreating the game.

These are the effects I'll talk about: the Mobius Strip, the Rainbow, and the Infinite Loop Background.

The luxe and haxe features I used were: geometry, static extensions, colors, transforms, timers, and batchers.

Mobius Runner Screenshot

The Möbius Strip: Geometry and Static Extensions

The player's avatar (a white circle) travels around the rainbow mobius strip during the game, so naturally it was the first thing I made. To do so I extended luxe's built-in Visual class. The code that generates the mobius strip shows how easy it is to generate geometry in luxe.

public override function new(_options:luxe.options.VisualOptions, radius:Float) {  
        super(_options);

        loopRadius = radius;

        //make new geometry
        geometry = new Geometry({
            primitive_type: PrimitiveType.line_loop,
            batcher: _options.batcher
        });

        //calculate mobius loop points
        var loopCenter = new Vector(loopRadius, 0);
        for (i in 0 ... loopSteps) {
            var degrees = (i / loopSteps) * 360;
            degrees -= 180;
            var radians = Maths.radians(degrees);

            var angleVector = (new Vector()).setFromAngle(radians);

            var newPoint = Vector.Add(loopCenter, Vector.Multiply(angleVector, loopRadius));

            loopPoints.push(newPoint);
        }

        var loopCenter = new Vector(-loopRadius, 0);
        for (i in 0 ... loopSteps) {
            var degrees = (i / loopSteps) * 360;
            var radians = Maths.radians(degrees);

            var angleVector = (new Vector()).setFromAngle(radians);
            angleVector.y *= -1;

            var newPoint = Vector.Add(loopCenter, Vector.Multiply(angleVector, loopRadius));

            loopPoints.push(newPoint);
        }

        //add mobius loop points to geometry
        var i = 0;
        for (p in loopPoints) {
            geometry.add(new Vertex(p));
        }
    }

The first for-loop builds the right half of the mobius loop. The second for-loop builds the left half of the mobius loop.

An interesting line of code you might not have noticed was var angleVector = (new Vector()).setFromAngle(radians);. The purpose of the code is straightforward: it generates a unit vector in 2 dimensions based on an angle. But setFromAngle() is not a method built into luxe's Vector class. Instead it's a method I wrote in a custom static extension. Extensions are a really handy feature of Haxe that let you add methods to pre-existing classes (more info on the Haxe website). That class just has to match the type of the first parameter in your extension method. Here is the extension I wrote:

class VectorExtender {  
    static public function distance(pos1:Vector, pos2:Vector) : Float {
        return Vector.Subtract(pos1, pos2).length;
    }

    static public function setFromAngle(v:Vector, radians:Float) : Vector {
        v = new Vector(Math.cos(radians), Math.sin(radians));
        return v;
    }
}

You can import an extender like this: using packagename.VectorExtender;. After you've done that, any Vector object I make can use distance() or setFromAngle() as if those methods were part of the original class.

The Rainbow Effect: Color

If you play the game, you'll see the mobius strip is constantly changing colors. Luxe makes it very easy to use colors with its built in Color and ColorHSV classes. Using ColorHSV (see the luxe color guide for an explanation of HSV color) I can simply iterate over all the vertices that make up the mobius strip (the points calculated in the previous section) and change the hue as I go, making a smooth rainbow effect. The mobius loop extends Visual which extends Entity which has a built-in update loop. In the update loop, I change the starting hue, which causes the colors to animate.

The code is straight-forward:

public override function update(dt : Float) {  
    startHue = (startHue + (hueDelta * dt)) % 360;
    setColors();
}

function setColors() {  
    var count = 0;
    for (v in geometry.vertices) {
        var hue = 0.0;
        if (count < loopSteps) {
            hue = Maths.lerp(startHue, startHue + hueRange, count / loopSteps) % 360;
        }
        else {
            hue = Maths.lerp(startHue + hueRange, startHue, (count - loopSteps) / loopSteps) % 360;
        }

        v.color = new ColorHSV(hue, 1, 1);

        count++;
    }
}

I used similar code to change the background color over time.

The Receding Möbius Loop Background: Transforms, Timers, and Batchers

A few features of luxe allowed me to easily create the "infinite" receding mobius loop background effect. The first is that every Entity in luxe has a Transform which represents the entity's location in space (pos), its rotation (rotation), and its size (scale). To make the receding loops, I simply made copies of my mobius loop object, and then decreased its transform.scale vector at a steady rate. Here you can see how I shrunk the loops:

for (l in backLoopList) {  
    //shrink loop
    l.scale.subtract(new Vector(0.5 * dt, 0.5 * dt));

    //destroy loop if it gets too small
       if (l.scale.x < 0) {
        backLoopList.remove(l);
        l.destroy();
       }
}

Besides shrinking and destroying loops, I had to create new ones at regular intervals. Luxe has a handy timer class that made this exceptionally easy. When you call Luxe.timer.schedule() you supply it with an interval of time (in seconds), a callback function to run when the timer is finished, and a flag to tell it whether to repeat (true) or not (false). The code in this case is:

Luxe.timer.schedule(0.5, function() {  
        var newLoop = new MobiusLoop({batcher: backBatcher, group:1, pos: Luxe.screen.mid}, Luxe.screen.w / 5);
        newLoop.color = new Color(0,0,0);
        newLoop.scale = new Vector(4,4);
        backLoopList.push(newLoop);
    }, true);

The last feature of luxe that came in handy for this effect was batchers. Batchers are responsible for taking groups of geometry (like these loops) and rendering them on the screen. Because batchers work with groups, you can use them to make changes to a large number of objects at once. In this case, I used the batcher to change the line width of the background loops versus the foreground loop (the background loops are much thinner). I also set the background batcher on a lower layer, so that its loops are always rendered behind the foreground loop. Batchers are a good way to do things like put UI on its own layer.

This is how I made the batcher:

var backBatcher = Luxe.renderer.create_batcher({name: "backBatcher", layer: -1});  
backBatcher.add_group(1, pre_group1, post_group1);  

The layer: -1 puts the batcher on a layer behind the main batcher.

The add_group() method lets me assign geometry to a particular group in the batcher with its own pre-render and post-render functions. I used those functions to change the global line width variable to a thin width and back again, like so:

    function pre_group1(_) {
        //set thin line width for background loops
        Luxe.renderer.state.lineWidth(1);
    }
    function post_group1(_) {
        //go back to thick line width for foreground
        Luxe.renderer.state.lineWidth(4);
    }

To add my loops to the batcher and to the right geometry group I just have to make sure my geometry options include batcher: correctBatcher and group: groupIdNumber. Like so:

var newLoop = new MobiusLoop({batcher: backBatcher, group: 1, pos: Luxe.screen.mid}, Luxe.screen.w / 5);

The end!

That's all I've got for now, but if you have any comments or feedback on the game, or questions about my code, please feel free to post below!

--- Adam Le Doux

Tags
luxegeometrygame jamextenderscolortransformtimerbatchers