Let’s Build Pong in Vortex Engine!

In our last post, we presented the new event system in Vortex and how it allows scripts to receive and react to input events. This week we want to put this system to the test by showing how an interactive playground could be implemented. Enter Vortex Pong!

To see the playground in action, please check out the video above. In the rest of this post, I’m going to break down how the project was built and how the script running the simulation works.

Visual Layout of the World

We first started by modeling the environment. Visuals are not a requirement for a pong-like game, as long as you have two paddles and a ball, but we also wanted to show how easy it is to have realtime dynamic lights in the scene.

Laying out the world for our pong-like simulation. Vortex Editor allows visually assembling our scene from simpler objects and placing the lights.

Laying out the world for our pong-like simulation. Vortex Editor allows visually assembling our scene from simpler objects and placing the lights.

Vortex Engine enables visually creating the layout of the world and objects inside it. For representing the ball, we used a simple sphere primitive, whereas the rest of the geometry was assembled from scaled cubes.

The Vortex Editor allows easily resizing the meshes into the appropriate shapes and creating materials that interact with the lights. We also used the editor to place the lights in the scene.


No matter how good we might be able to make the world look, it won’t do a lot without adding logic to it.

In Vortex, we use Lua for scripting. The Engine provides a runtime to load and execute scripts in the project and it exposes an API that can be used to run a simulation loop and handle events – This is really all that’s needed for this playground!

The Problem Space

We don’t want to go overboard with our implementation, so we will keep everything simple by having 3 functions and a tiny self-contained vector math library.

The ball will bounce around, updated in our simulation function. We want to support having two players. For this, we will store the state of the keyboard as events arrive, and update the paddle positions also from our simulation loop.

Lua is a pretty great language and in under 200 lines of code we are able to build everything we need for this.


Initialization happens once at the beginning of the script execution and it is responsible for setting up the main camera, registering callbacks for handling on_frame and on_event messages and finding and caching entities of interest.

function main()
    -- register ourselves for the engine callbacks:
    table.insert( vtx.callbacks.on_frame, on_frame )
    table.insert( vtx.callbacks.on_event, on_event )

    -- set the main camera (this is also needed for events to be sent to us)
    local cam_entity = vtx.find_first_entity_by_name( "main_cam" )
    local cam = cam_entity:first_component_of_type( TYPE_CAMERA )
    vtx.rendering.set_main_camera( cam )

    -- find entities of interest and cache their transforms:
    local ball = vtx.find_first_entity_by_name( "ball" )
    ball_xform = ball:get_transform()
    local bsx, bsy, bsz = ball_xform:get_scale()
    ball_scale = bsx

    local paddle_left = vtx.find_first_entity_by_name( "paddle_left" )
    paddle_left_xform = paddle_left:get_transform()

    local paddle_right = vtx.find_first_entity_by_name( "paddle_right" )
    paddle_right_xform = paddle_right:get_transform()

    -- get and store the position and bounds of the world
    local world_container = vtx.find_first_entity_by_name( "world_container" )
    world_container_xform = world_container:get_transform()
    local wx, wy, wz, ww = world_container_xform:get_position()
    world_center = vec2.new( wx, wy )
    local wsx, wsy, wsz = world_container_xform:get_scale()
    world_size = vec2.new( wsx, wsy )

    -- start the ball (we could randomize this)
    ball_dir = vec2.random_non_zero():normalize()

    -- find animated lights:
    local ball_light = vtx.find_first_entity_by_name( "ball_light" )
    ball_light_xform = ball_light:get_transform()

    local paddle_left_light = vtx.find_first_entity_by_name( "paddle_left_light0" )
    paddle_left_light_xform = paddle_left_light:get_transform()

    local paddle_right_light = vtx.find_first_entity_by_name( "paddle_right_light0" )
    paddle_right_light_xform = paddle_right_light:get_transform()


Notice how the first two lines add our script functions to the engine’s on_frame and on_event callback list. This is the key for building an interactive simulation.

Handling Events

Handling events is pretty simple. We will hold a global table that stores which keys are currently pressed down on the keyboard. We do not update any paddle positions here. These updates will be handled in the simulation function.

function on_event( evt )
    if evt.type == EVT_TYPE_KEYDOWN then
        pressed_keys[ evt.key ] = true
    elseif evt.type == EVT_TYPE_KEYUP then
        pressed_keys[ evt.key ] = nil

Simulation Loop

The simulation is the most complicated function in this example. It has several responsibilities, including updating everything that is moving, detecting collisions against the world and the paddles, and resetting the game in case of a player scoring.

In the context of this example, we did not dig in deep into possible optimizations other than avoiding unnecessary table allocations. ALU (CPU time) could be saved by premultiplying radii and sizes by 0.5 as part of the initialization function.

In a real life example we would also want to break up this function into smaller ones with more clearly-defined responsibilities.

function on_frame( delta_t )
    --update ball:
    local x,y,z,w = ball_xform:get_position()

    local next_x = x + ball_dir.x * delta_t * ball_speed
    local next_y = y + ball_dir.y * delta_t * ball_speed

    local bounce_right = next_x + ball_scale * 0.5 >= world_center.x + world_size.x * 0.5
    local bounce_left =  next_x - ball_scale * 0.5 <= world_center.x - world_size.x * 0.5

    if bounce_left == false and bounce_right == false then
        x = next_x
        -- the ball has hit one of the horizontal walls, check for scoring event:
        if bounce_right then
            local px, py, pz, pw = paddle_right_xform:get_position()
            local sx, sy, sz = paddle_right_xform:get_scale()
            if y <= py + sy * 0.5 and y >= py - sy * 0.5 then
                ball_dir.x = -ball_dir.x -- saved!
                -- score!
                print( "Score Player 0!" )
                ball_dir = vec2.random_non_zero():normalize()
                x = world_center.x
                y = world_center.y
        elseif bounce_left then
            local px, py, pz, pw = paddle_left_xform:get_position()
            local sx, sy, sz = paddle_left_xform:get_scale()
            if y <= py + sy * 0.5 and y >= py - sy * 0.5 then
                ball_dir.x = -ball_dir.x -- saved!
                -- score!
                print( "Score Player 1!")
                ball_dir = vec2.random_non_zero():normalize()
                x = world_center.x
                y = world_center.y

    if next_y + ball_scale * 0.5 >= world_center.y + world_size.y * 0.5 or next_y - ball_scale * 0.5 <= world_center.y - world_size.y * 0.5 then
        ball_dir.y = -ball_dir.y
        y = next_y

    ball_xform:set_position( x, y, z, w )
    ball_light_xform:set_position( x, y, z, w )

    -- update paddles:
    local plx, ply, plz, plw = paddle_left_xform:get_position()
    if pressed_keys[ KEY_W ] ~= nil then
        ply = ply + delta_t * paddle_speed
    if pressed_keys[ KEY_S ] ~= nil then
        ply = ply - delta_t * paddle_speed
    paddle_left_xform:set_position( plx, ply, plz, plw )
    paddle_left_light_xform:set_position( plx, ply, plz, plw )

    local prx, pry, prz, prw = paddle_right_xform:get_position()
    if pressed_keys[ KEY_UP ] ~= nil then
        pry = pry + delta_t * paddle_speed
    if pressed_keys[ KEY_DOWN ] ~= nil then
        pry = pry - delta_t * paddle_speed
    paddle_right_xform:set_position( prx, pry, prz, prw )
    paddle_right_light_xform:set_position( prx, pry, prz, prw )


One of the first things you will notice is that all the logic is simulated in 2D, despite this being a 3D world. There is no need to run the simulation in 3D, as we are only interested in what's happening on the plane where the game is being played.

Lines 3-13, 43-47: the way the ball simulation works is by calculating the position where it should be next based on its move vector and the time elapsed since the last update. Instead of updating the ball position immediately, however, we check if the new positions would make us collide against a wall, the floor or the ceiling.

Colliding with the floor and ceiling is trivial: we just mirror the y component of the move vector.

Lines 14-41: Horizontal collisions are move complex, as they require checking if we hit the paddles or not.

Lines 49-50: Assuming no player has scored, we send the updated positions down to the engine via the transform objects we previously cached.

Lines 53-71: Finally, we deal with updating the paddle positions based on what keys are currently pressed. This allows for a very smooth animation experience for the player, much more so than updating the positions directly from the keypress event.


This is really all there is to it. Once everything is set up, we can run the playground from the Editor directly or build it into a Vortex Archive and run it on any Vortex Runtime (any Runtime that has a keyboard attached that is).

If you haven't seen the video above, I recommend you take a look, as I mention a few concepts that I glanced over in this post.

I hope you guys found this post interesting and, as usual, stay tuned for more!

Event Handling via Scripts

This week work went into adding support for receiving events and passing them down to scripts. The idea is to allow scripts to provide some logic that reacts to user actions.

Vortex has never had an event system before. When the time to design one came, we determined that we wanted a flexible and extensible system that allowed modeling different kinds of events generated on different platforms.

In order to make it extensible it was decided to go with an object-oriented design. A base Event class would provide the general interface to all events and specialized sub-classes could model specific events happening on the hardware, such as a keypress, a mouse move, or a finger swipe.

namespace vtx
	class Event
		virtual ~Event() { }
		virtual EventType type() const = 0;

	class KeyboardEvent : public Event
		KeyboardEvent( short key );
		short key() const;
		short _key;

	class KeyPressedEvent : public KeyboardEvent
		KeyPressedEvent( short key );
		virtual EventType type() const override;

// ...etc...

With this simple interface in place, we can have platform-specific code wrap events in instances of these classes and pass them to the engine manager (a front controller). The engine manager takes care of moving data around so that, eventually, events make it to Lua scripts.

Handling Events from Lua

In order to receive events, scripts must add a handler function to the vtx.callbacks.on_event list. If you remember from this previous post on scripting, we were already using the vtx.callbacks namespace as a means for scripts to register a function to be called every frame. This mechanism extends on the idea.

When an event is passed to the engine, functions registered to the on_event list are called. The functions are called with a single argument: a table containing the event properties (such as which key was pressed for a keydown event).

The following example shows this interaction:

function on_event( evt )
    print( "Received event: type: " .. evt.type .. " key: " .. evt.key )

    if evt.type == 0 then
        x,y,z,w = box_xform:get_position()

        if evt.key == vtx.Event.KEY_RIGHT then
            x = x + 0.1
        elseif evt.key == vtx.Event.KEY_LEFT then
            x = x - 0.1
        elseif evt.key == vtx.Event.KEY_UP then
            y = y + 0.1
        elseif evt.key == vtx.Event.KEY_DOWN then
            y = y - 0.1

-- Somewhere:
table.insert( vtx.callbacks.on_event, on_event )

The above example could be used to move an entity in the scene in response to keyboard events. Of course, in practice keyboard event handling should be dependent on time elapsed, and we probably want to handle key autorepeat too, but for this example, it clearly shows how event data can be extracted from the parameter supplied to the callback.

Events are Transient Objects

Events are handled a bit differently from everything else in the engine so far.

Unlike other engine objects, we do not expect scripts (or other subsystems) to hold on to the engine object representing the instant event. The event should be handled when it occurs. By realizing this, we notice that there is no need to keep a native pointer to the event in the Lua table.

Instead, we can simplify by creating a table and copying the important information directly into it. This way we also avoid roundtripping into the engine to fetch all event properties.

Internally, at the engine level, the event object is transient. It lives in the stack and is destroyed automatically after it’s been dispatched to all its handlers.

Handling Events in the Editor

It is important to note that when running in Editor, scripts must define a main camera in order for events to be delivered to the scripts.

This might sound counterintuitive, but it was a deliberate decision to preserve the fly-over cam controls in Editor if a script-driven camera was never defined. This is important for authoring a playground where we don’t want a script to take over the camera.


In the search of a perfect solution, I had been putting off adding event handling for a while. In the end, the solution devised solves our current needs in a simple and (hopefully) elegant fashion.

Now that we have a camera control API and event handling from scripts, we are in a good position for Vortex playgrounds to take over control and start exposing more elaborate functionality.

I’m excited for what’s to come. Stay tuned for more!