Tick()
functions to update game objects independently of eventsIn our previous projects, we’ve updated the state of our objects based on events detected in our application loop:
// Application Loop
while (shouldContinue) {
// Handle Events
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_QUIT) {
shouldContinue = false;
}
GameObject.HandleEvent(Event);
}
// Render Objects
GameWindow.Render();
GameObject.Render(GameWindow.GetSurface());
// Update Frame
GameWindow.Update();
}
However, in more complex projects, most objects may need to update their state consistently, even when they’re not receiving any events. For example, characters not controlled by our player may need to continue to act and move, and animation, and visual effects should continue to update.
To implement this capability, our applications can introduce the notion of ticking. On every iteration of our application loop, we call a function on our objects that allows them to update their state.
These are commonly called tick functions:
// Application Loop
while (shouldContinue) {
// Handle Events
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_QUIT) {
shouldContinue = false;
}
GameObject.HandleEvent(Event);
}
// Update Objects
GameObject.Tick();
// Render Objects
GameWindow.Render();
GameObject.Render(GameWindow.GetSurface());
// Update Frame
GameWindow.Update();
}
Complex projects can have thousands or millions of objects that are ticking on every frame, so we commonly use an intermediate object to manage this complexity
Let’s have all of our game objects inherit from a standard base class:
// GameObject.h
#pragma once
#include "SDL.h"
class GameObject {
public:
virtual void HandleEvent(SDL_Event& E) {
// ...
}
virtual void Tick() {
// ...
}
virtual void Render(SDL_Surface* Surface) {
// ...
}
};
We’ll create a World
class that manages all of the game objects in our world. When our main application loop prompts our world to handle an event, tick, or render, we’ll forward that instruction to all of our objects:
// World.h
#pragma once
#include <vector>
#include <memory>
#include "GameObject.h"
class World {
public:
void HandleEvent(SDL_Event& E) {
for (auto& Object : Objects) {
Object->HandleEvent(E);
}
}
void Tick() {
for (auto& Object : Objects) {
Object->Tick();
}
}
void Render(SDL_Surface* Surface) {
for (auto& Object : Objects) {
Object->Render(Surface);
}
}
private:
std::vector<std::unique_ptr<GameObject>> Objects;
}
Let’s update our main application loop to use our new class:
// main.cpp
#include <SDL.h>
#include "Window.h"
#include "World.h"
int main(int argc, char** argv){
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
World GameWorld;
SDL_Event Event;
bool shouldContinue{true};
// Application Loop
while (shouldContinue) {
// Handle Events
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_QUIT) {
shouldContinue = false;
}
GameWorld.HandleEvent(Event);
}
// Update Objects
GameWorld.Tick();
// Render Objects
GameWindow.Render();
GameWorld.Render(GameWindow.GetSurface());
// Update Frame
GameWindow.Update();
}
SDL_Quit();
return 0;
}
We can now use inheritance and polymorphism (overriding virtual
functions) to support a variety of GameObject
types. Let’s update our program to support Goblin
objects:
// Goblin.h
#pragma once
#include <string>
#include "GameObject.h"
class Goblin : public GameObject {
public:
Goblin(const std::string& name, int x, int y)
: Name(name), xPosition(x), yPosition(y) {}
std::string Name;
int xPosition;
int yPosition;
};
We’ll add a SpawnGoblin()
method to the World
class, which will create a GameObject
managed by that World
when called. This function will also return a reference to the spawned object, so callers can access it if needed:
// World.h
// ...
class World {
public:
Goblin& SpawnGoblin(
const std::string& Name, int x, int y
) {
Objects.emplace_back(
std::make_unique<Goblin>(Name, x, y)
);
return static_cast<Goblin&>(*Objects.back());
}
// ...
private:
std::vector<std::unique_ptr<GameObject>> Objects;
};
// main.cpp
// ...
int main(int argc, char** argv){
// ...
World GameWorld;
Goblin& Enemy{GameWorld.SpawnGoblin(
"Goblin Rogue", 100, 200)};
std::cout << "A " << Enemy.Name
<< " was spawned in the world";
// ...
}
A Goblin Rogue was spawned in the world
Finally, we can now override the Tick()
function for our Goblin
objects. Let’s allow our goblins to move regardless of whether any events are happening:
// Goblin.h
#pragma once
#include <string>
#include "GameObject.h"
class Goblin : public GameObject {
public:
Goblin(const std::string& name, int x, int y)
: Name(name), xPosition(x), yPosition(y) {}
std::string Name;
int xPosition;
int yPosition;
void Tick() override {
std::cout << "\nTick() updating position";
xPosition += 1;
}
void Render(SDL_Surface* Surface) override {
std::cout
<< " - Rendering at x = " << xPosition;
// ...
}
};
A Goblin Rogue was spawned in the world
Tick() updating position - Rendering at x = 101
Tick() updating position - Rendering at x = 102
Tick() updating position - Rendering at x = 103
...
If needed, they can also react to events in addition to ticking. When the player presses an arrow key, let’s change the direction our Goblin
moves on each tick:
// Goblin.h
// ...
class Goblin : public GameObject {
public:
// ...
int Velocity{1};
void HandleEvent(SDL_Event& E) {
if (E.type != SDL_KEYDOWN) return;
if (E.key.keysym.sym == SDLK_RIGHT) {
std::cout << "\nHandleEvent() setting "
"velocity to 1";
Velocity = 1;
} else if (E.key.keysym.sym == SDLK_LEFT) {
std::cout << "\nHandleEvent() setting "
"velocity to -1";
Velocity = -1;
}
}
void Tick() override {
std::cout << "\nTick() updating position";
xPosition += Velocity;
}
// ...
};
A Goblin Rogue was spawned in the world
Tick() updating position - Rendering at x = 101
Tick() updating position - Rendering at x = 102
HandleEvent() setting velocity to -1
Tick() updating position - Rendering at x = 101
...
The previous example is setting our object’s velocity in terms of ticks - that is, our object is moving by one unit per tick. This is generally not recommended, as we do not know how much time has passed between invocations of Tick()
.
If our game can complete iterations of its application loop more quickly, that means our Tick()
functions will be called more frequently, and our objects will therefore move faster than we intended.
In the next lesson, we’ll expand our tick functions to include time deltas. This allows us to implement our behaviors in terms of consistent, real-world units such as seconds and milliseconds.
In complex projects, updating objects often depends on the state of other objects in the world. For example, consider a UI element that appears attached to one of our game objects:
// NameTag.h
#pragma once
#include "GameObject.h"
#include "Goblin.h"
class NameTag : public GameObject {
public:
Goblin& Parent;
void Tick() override {
// Are these arguments correct?
// It's unclear whether Parent has ticked yet
SomeFunction(
Parent.xPosition,
Parent.yPosition
)
}
};
The issue here is that we don't know the order in which our objects' Tick()
functions are called. If Parent
ticks before NameTag
, there's no problem. However, if NameTag
ticks first, the Parent.xPosition
and Parent.yPosition
values will not have been updated yet, resulting in stale data.
Using these stale values means our NameTag
's position will be based on where the Goblin
was in the previous frame, not the current frame. As a result, our NameTag
will lag one frame behind the Goblin
object it's supposed to be attached to.
These off-by-one-frame issues are extremely common, even in major released projects. They're difficult to notice explicitly, especially when many things are happening on-screen simultaneously. However, they contribute to a general feeling that our game is less responsive than it should be, so it's worth preventing these problems where possible.
In complex projects, the architecture to manage these inter-object dependencies can get quite elaborate. A common and simple first step involves breaking our tick process into multiple phases and establishing a convention on what type of updates should be performed in each phase.
For example, we could split our ticking into two phases: TickPhysics()
, followed by TickUI()
:
// Application Loop
while (shouldContinue) {
// Handle Events
while (SDL_PollEvent(&Event)) {
// ...
}
// Update Objects
GameWorld.TickPhysics();
GameWorld.TickUI();
// Render Objects
GameWindow.Render();
GameWorld.Render(GameWindow.GetSurface());
// Update Frame
GameWindow.Update();
}
If we then establish the convention that any code that updates the physical state of our world belongs in TickPhysics()
, any logic in an overloaded TickUI()
function can be confident that those world positions are up to date:
// NameTag.h
// ...
class NameTag : public GameObject {
public:
NameTag(const Goblin& Parent)
: Parent(Parent) {}
Goblin& Parent;
int xPosition;
int yPosition;
void TickUI() override {
// We know these values have been updated
// because TickPhysics() happens before TickUI()
xPosition = Parent.xPosition;
yPosition = Parent.yPosition;
}
};
The GameWorld.SpawnGoblin(Name, x, y)
approach in this section was used to simplify memory management. It absolves consumers from needing to manage the lifecycle of their GameObject
instances, as it is all handled by the World
class.
However, larger projects can have hundreds or thousands of GameObject
subtypes. Therefore, scaling this technique would involve adding an additional method to our World
class for each subtype, and potentially multiple methods if the type has multiple constructors.
// World.h
// ...
class World {
public:
Goblin& SpawnGoblin(int Arg1, int Arg2) {
// ...
}
Dragon& SpawnDragon(int Arg1) {
// ...
}
// The Dragon type has multiple constructors
Dragon& SpawnDragon(int Arg1, float Arg2) {
// ...
}
// ...
}
We could solve this by creating objects outside of our World
, and then transferring ownership:
// World.h
// ...
class World {
public:
void AddObject(std::unique_ptr<GameObject> Object) {
// ...
}
}
auto Enemy1{std::make_unique<Goblin>(100, 200)};
World.AddObject(std::move(Enemy1));
auto Enemy2{std::make_unique<Dragon>(50)};
World.AddObject(std::move(Enemy2));
auto Enemy3{std::make_unique<Dragon>(100, 0.5f)};
World.AddObject(std::move(Enemy3));
We can make this API simpler and less error-prone. For comparison, the Unreal Engine API for creating objects in the world looks like this:
GetWorld()->SpawnActor<Goblin>(100, 200);
GetWorld()->SpawnActor<Dragon>(50);
GetWorld()->SpawnActor<Dragon>(100, 0.5f);
However, implementing this requires more complex C++ techniques that may be unfamiliar:
// World.h
// ...
class World {
public:
template <typename T, typename... Args>
T& SpawnObject(Args&&... args) {
Objects.emplace_back(std::make_unique<T>(
std::forward<Args>(args)...));
return static_cast<T&>(*Objects.back());
}
// ...
};
We introduce these techniques and cover them in more detail in our advanced course. They include:
<
and >
tokens. In this example, we’re using the Goblin
type as a template argument....
syntax, which allows us to create variadic functions. Variadic functions can handle a variable number of parameters. In this example, we’re collecting an unknown number of parameters to forward to the constructor of the type we passed as a template argument.std::forward
function, which helps us forward parameters from one function to another in a way that respects the value category of the original argument.Our more flexible API can be used like this:
// main.cpp
// ...
int main(int argc, char** argv){
// ...
World GameWorld;
Player& PlayerOne{
GameWorld.SpawnObject<Player>(
"Player One"
)};
Goblin& Enemy{
GameWorld.SpawnObject<Goblin>(
"Goblin", 100, 200
)};
// ...
}
This lesson introduced the concept of ticking in game development using C++ and SDL2:
GameObject
and World
classes to manage multiple game objects and remove complexity from our application loop.Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games