Update: It is also possible run the demo with the following link: https://zetscript.org/demos/zs_ge/engine.html
Introduction
This article aims to show how easily you can build a simple game engine in C++ application with SDL2 and ZetScript script engine. The engine will have a few set of functions for paint graphics, read input keys and play sounds using Simple DirectMedia Layer(SDL). Later, we will show some pieces of script code about how to make a demo of Invader game.
Quote:
Note: This article is not going to go in depth detail of function implementations for the engine we are presenting in this article but it will show class headers and some pieces of code from main function to have a basic idea how everything fits together.
How to Use the Demo
In order to run the Invader demo game, execute the following steps:
- Decompress zs_ge-1.1.0-x86-win32.zip in some directory
- Drag invader.zs into engine.exe
Controls
- Left/Right: Move ship
- Space: Start game/shoot
- F5: Reload script invader.zs
- F9: Toggle fullscreen
- ESC: Quit engine
Requirements
To compile the code, you need ZetScript library, SDL2 library, cmake application, and MinGW or Linux with gnu 4.8 compiler or MSVC 2015/2017 or build tools v141.
If you satisfy the requirements, go to the directory where you have decompressed the source code and do the following:
cmake
After cmake
operation, the configuration files will be created.
Quote:
Note: In case of configure project for MVC++, you also have to provide include paths and library paths in order to find SDL and ZetScript.
Engine
The version of the engine we are presenting in this article will draw graphics, sprites, fonts, will play sounds and will read input keys from keyboard. The engine has the following classes:
Input
Image
Sprite
Font
Render
Sound
SoundPlayer
Input
CInput
class implements functions to read input keys.
#define T_ESC Input::getInstance()->key[KEY_ESCAPE]
#define T_F5 Input::getInstance()->key[KEY_F5]
#define T_F9 Input::getInstance()->key[KEY_F9]
#define T_SPACE Input::getInstance()->key[KEY_SPACE]
#define TR_UP Input::getInstance()->keyR[KEY_UP]
#define TR_LEFT Input::getInstance()->keyR[KEY_LEFT]
#define TR_RIGHT Input::getInstance()->keyR[KEY_RIGHT]
#define TR_DOWN Input::getInstance()->keyR[KEY_DOWN]
typedef struct{
Uint32 codeKey;
}EventKey,EventRepeatKey;
class Input{
static Input *input;
SDL_Event Event;
public:
bool key[KEY_LAST];
bool keyR[KEY_LAST];
static CInput * getInstance();
static void destroy();
void update();
};
List 1.1
Image
CImage
class implements functions to load bmp images and create dynamic images based on binary indices.
#pragma once
#include <SDL2/SDL.h>
#include <zetscript.h>
class Image{
protected:
SDL_Texture *texture;
int width,height;
static SDL_Texture * SurfaceToTexture(SDL_Surface *srf);
bool createSquarePixmap(const vector<int> & pixmap);
void destroy();
public:
Image();
Image(const vector<int> & pixmap);
bool load(const char *file);
SDL_Texture *getTexture();
bool createSquarePixmap(zetscript::CVectorScriptVariable *vector);
int getWidth();
int getHeight();
~Image();
};
List 1.2
Sprite
CSprite
class implements functions to setup and update sprites in a xy coordinate and paints it the current frame it has.
#include "Image.h"
class Sprite{
static int synch_time;
unsigned current_frame;
int current_time_frame,time_frame;
public:
typedef struct{
Image *image;
Uint32 color;
}FrameInfo;
static void synchTime();
int x, y;
int dx, dy;
int width, height;
std::vector<FrameInfo> frame;
static bool checkCollision(Sprite *spr1, Sprite *spr2);
static bool checkCollision(int offset_x,int offset_y,CSprite *spr1, CSprite *spr2);
CSprite();
void addFrame(CImage *fr, int rgb);
void setFrame(int n);
void setTimeFrame(int time);
int getWidth();
int getHeight();
Sprite::FrameInfo * getCurrentFrame();
void update();
~Sprite();
};
List 1.3
Font
CFont
class implements functions to load bmp fonts. The user has to pass the dimension of each character in order to correctly align all characters it renders.
#include "CImage.h"
class Font:public Image{
int totalchars_x, totalchars_y,totalchars;
SDL_Rect m_aux_rect;
int char_width,char_height;
public:
CFont();
bool load(const char * file,int char_width,int char_height);
int getCharWidth();
int getCharHeight();
int getTextWith(const string & str);
SDL_Rect * getRectChar(char c);
~CFont();
};
List 1.4
Render
CRender
class implements functions to paint images, sprites and texts using a font.
class Render{
int width, height;
static Render *render;
SDL_Renderer *p_renderer = NULL;
SDL_Event event;
bool fullscreen;
SDL_Window* p_window = NULL;
Render();
~Render();
public:
static Render *getInstance();
static void destroy();
int getWidth();
int getHeight();
void createWindow(int width, int height);
void toggleFullscreen();
SDL_Renderer *getRenderer();
SDL_Window *getWindow();
void clear(Uint8 r, Uint8 g, Uint8 b);
void drawImage(int x, int y, CImage *img);
void drawImage(int x, int y, CImage *img, int color);
void drawText(int x,int y, CFont * font, const char * text);
void drawSprite(CSprite *spr);
void drawSprite(int x, int y, CSprite *spr);
void update();
};
List 1.5
CSound
CSound
class implements function to load sound file (only supports wave).
class Sound{
public:
Uint32 wav_length;
Uint8 *wav_buffer;
Sound();
bool load(string * file);
~Sound();
};
List 1.6
SoundPlayer
CSoundPlayer
implements functions to play CSound
objects and init the sound system.
#define MAX_PLAYING_SOUNDS 20
class SoundPlayer{
SDL_AudioSpec wav_spec;
typedef struct {
Uint8 *audio_pos;
Uint32 audio_len;
}SoundData;
static SoundData SoundData[MAX_PLAYING_SOUNDS];
static SoundPlayer * singleton;
SDL_AudioDeviceID dev;
SoundPlayer();
~SoundPlayer();
static void callbackAudio(void *userdata, Uint8* stream, int len);
public:
static SoundPlayer * getInstance();
static void destroy();
void setup(SDL_AudioFormat format=AUDIO_S16SYS,
Uint16 Freq=22050, Uint16 samples=4096, Uint8 channels=2);
void play(CSound *snd);
};
List 1.6
Bind C++ Code to Use in Script
In the last sections, we have seen the classes that make up the functionality to manage graphics, input and sound for the game engine. This section and maybe the most exciting part, we will see how easy it is to bind main parts of the C++ headers to use in script side through ZetScript
API.
We have register_C_Class
and register_C_SingletonClass
to register classes and singletons respectively. register_C_VariableMember
, register_C_FunctionMember
for registering its class members. Also, register_C_FunctionMember
is used to register C functions.
For example, we register CImage
to use in the script by doing the following:
registerNativeClass<CImage>("CImage")
And in the similar way to register its function member CImage::load
:
registerNativeFunctionMember<CImage>("load",&CImage::load);
The following code shows all the functions and variables we need to be used in the script side. We will see that in a few lines of code, we have registered them all!
registerNativeClass<CImage>("Image")) return false;
registerNativeFunctionMember<Image>("Image",static_cast<bool
(Image::*)(zetscript::CVectorScriptVariable * )>(&Image::createSquarePixmap));
registerNativeFunctionMember<Image>("load",&Image::load);
registerNativeClass<Sprite>("Sprite");
registerNativeFunction("checkCollision",static_cast<bool
(*)(int, int, Sprite *, Sprite *)>(&Sprite::checkCollision));
registerNativeFunction("checkCollision",static_cast<bool
(*)(Sprite *, Sprite *)>(&Sprite::checkCollision));
registerNativeVariableMember<Sprite>("x",&Sprite::x);
registerNativeVariableMember<Sprite>("y",&Sprite::y);
registerNativeVariableMember<Sprite>("dx",&Sprite::dx);
registerNativeVariableMember<Sprite>("dy",&Sprite::dy);
registerNativeVariableMember<Sprite>("width",&Sprite::width);
registerNativeVariableMember<Sprite>("height",&Sprite::height);
registerNativeFunctionMember<Sprite>("setTimeFrame",&Sprite::setTimeFrame);
registerNativeFunctionMember<Sprite>("update",&Sprite::update);
registerNativeFunctionMember<Sprite>("addFrame",&Sprite::addFrame);
registerNativeClass<Font>("Font");
registerNativeFunctionMember<Font>("load",&Font::load);
registerNativeClass<Sound>("Sound");
registerNativeFunctionMember<Sound>("load",&Sound::load);
registerNativeSingletonClass<Render>("Render");
registerNativeFunction("getRender",Render::getInstance);
registerNativeFunctionMember<Render>
("drawImage",static_cast<void (Render:: *)(int, int, Image *)>(&Render::drawImage));
registerNativeFunctionMember<Render>
("drawImage",static_cast<void (Render:: *)(int, int, Image *,int)>(&Render::drawImage));
registerNativeFunctionMember<Render>("drawText",&Render::drawText);
registerNativeFunctionMember<Render>
("drawSprite",static_cast<void (Render:: *)(int, int, Sprite *)>(&Render::drawSprite));
registerNativeFunctionMember<Render>
("drawSprite",static_cast<void (Render:: *)(Sprite *)>(&Render::drawSprite));
registerNativeFunctionMember<Render>("getWidth",&Render::getWidth);
registerNativeFunctionMember<Render>("getHeight",&Render::getHeight);
registerNativeSingletonClass<SoundPlayer>("SoundPlayer");
registerNativeFunction("getSoundPlayer",&SoundPlayer::getInstance);
registerNativeFunctionMember<SoundPlayer>("play",&SoundPlayer::play);
registerNativeVariable("TR_UP",TR_UP);
registerNativeVariable("TR_DOWN",TR_DOWN);
registerNativeVariable("TR_LEFT",TR_LEFT);
registerNativeVariable("TR_RIGHT",TR_RIGHT);
registerNativeVariable("T_SPACE",T_SPACE);
List 1.7
Load Script File
ZetScript
provides a function to load script file called eval_file as we can see in the following code:
zs.evalFile("file.zs");
Bind Script Functions
After the file is loaded, we need to expose the script functions from the loaded file to be used in game engine. The script file should implement these three functions:
init
: will initialize variables, structs, sprites, load images, etc. update
: will be called in the C++ game engine loop unload
: will be called when the game is unloaded
To bind a script
function is done by bind_function
passing the function cast we want to be in the script side. Generally, all script
function does won't pass any arguments and won't return any value so the function cast will be void(void)
.
To bind these functions is shown in the following code:
std::function<void()> * init=zs.bindScriptFunction<void ()>("init");
std::function<void()> * update=zs.bindScriptFunction<void ()>("update");
std::function<void()> * unload=zs.bindScriptFunction<void ()>("unload");
List 1.8
Game Engine loop
Finally, we present the game engine loop. It would be the following code:
render=Render::getInstance();
input=Input::getInstance();
(*init)();
do{
render->clear(0,0,0);
input->update();
(*update)();
render->update();
}while(!T_ESC);
(*unload)();
List 1.9
Reevaluating Script File
As we said before, some time is interesting to reevaluate script file for developing purposes. Is faster reevaluate in the fly than reexecute the engine, pressing simple key. We modify the code 1.8 presented as follows:
do{
render->clear(0,0,0);
input->update();
if(T_F5) {
try{
(*unload)();
zetscript->evalFile(argv[1])){
init=bindScriptfunction<void ()>("init");
update=bind_function<void ()>("update");
unload=bind_function<void ()>("unload");
(*init)();
}catch(std::exception & ex){
fprintf(stderr,"%s",ex.what());
}
}
try{
(*update)();
}catch(std::exception & ex){
fprintf(stderr,"%s",ex.what());
}
render->update();
}while(!T_ESC);
List 1.10
The code 1.10 implements a refresh behaviour like it was as browser, i.e., when the user presses the F5 key, the script is reevaluated. The binding functions ini
, update
and unload
must be recreated due to changes in memory after reevaluating the file.
Invader Game
We have presented the implementation of our C++ game engine to process a implemented games from script files. Now, we will show how to implement an Invader
game using the game engine.
First of all, as we have mentioned before, the engine expects to implement three functions in the script side:
These three functions are declared in the file as shown in the following code:
function init(){
}
function update(){
}
function unload(){
}
List 2.1
Loading Images
The engine supports two types of images: binary and bitmap images.
To load bitmap, we can use CImage::load
so for example Invader
game loads title game bitmap called title.bmp. The variable declaration and its loading is done doing the following:
var invader_title;
function init(){
invader_title=new Image();
invader_title.load("invader_title.bmp");
}
To load bit based images, we have to use the CImage
constructor function CImage::createScquarePixmap
we have registered.
If we remember the code from list 1.7, we registered CImage::createSquarePixmap
function member called as CImage
(the same name as the class),
registerNativeFunctionMember<Image>("Image",static_cast<bool
(Image::*)(zetscript::CVectorScriptVariable * )>(&Image::createSquarePixmap));
What it means is that CImage::createSquarePixmap
is declared as CImage
constructor in script side because the function name exposed in script side is the same as the class name, i.e., CImage
. CVectorScriptVariable
is the vector type of ZetScript
(for further information, visit zetscript.org).
For example, the following code creates CImage
passing a vector with binary integers as parameter:
new Image( <-- Image constructor
[ 00100000100b
,10010001001b
,10111111101b
,11101110111b
,11111111111b
,01111111110b
,00100000100b
,01000000010b
]
)
List 2.2
That it represents the following image:
The 1s in binary format paints and 0s doesn't.
Quote:
Note: The game implements more images but to not fill too much these article with code, I keep out them.
Implementing Our Custom Sprite Class
We have seen in list 1.3 the Sprite
class. ZetScript
has the feature to inherit C++ classes. We define MyClassSprite
class that inherits Sprite
(from C++):
class MySprite: Sprite{
constructor(){
this.time_life=0;
this.attack_time=0;
this.points=undefined;
this.active=false;
this.color=0xFFFFFF;
}
constructor(_points,_color){
this.active=false;
this.points=_points;
this.color=_color;
this.time_life = 0;
this.attack_time=0;
}
addFrame(_img){
super(_img, this.color);
}
update(){
if(this.time_life>0){
if(this.time_life < currentTime()){
this.active=false;
}
}
super();
}
};
List 2.3
Sprite Manager
After we have defined our custom MySprite
class for script, we are presenting another important class that will manage the logic flow of sprite type. SpriteManager
will create and update a set of sprites of the same type.
In invader
game, we have the following sprite types:
- Enemy bullets sprites
- Hero bullets sprites
- Enemy sprites
In order to explain better how SpriteManager
works, we present a simplified code of SpriteManager
class with the important content from the original source:
class SpriteManager{
constructor(max_sprites, image,_max_time_life){
this.x_base=0;
this.y_base=0;
this.dx=0;
this.dy=0;
this.next_mov=0;
this.sprite=[];
this.free_index=[];
this.max_time_life=_max_time_life;
for(var i=0; i < max_sprites; i++){
var spr=new CMySprite();
spr.addFrame(image);
this.sprite.add(spr);
this.free_index.add(i);
}
}
create(start_x, start_y, _dx, _dy){
var index;
if(this.free_index.size()>0)
{
index= this.free_index.pop();
this.sprite[index].x= start_x;
this.sprite[index].y=start_y;
this.sprite[index].dy=_dy;
this.sprite[index].dx=_dx;
this.sprite[index].active=true;
if(this.max_time_life>0){
this.sprite[index].time_life=currentTime()+this.max_time_life;
}
}
}
checkCollision(spr)
{
}
doAttack(spr){
}
remove(i){
this.sprite[i].active=false;
this.free_index.add(i);
}
update(){
for(var i=0; i < this.sprite.size(); i++)
{
var spr=this.sprite[i];
if(spr.active){
this.checkCollision(spr);
spr.update();
this.doAttack(spr);
if(
(spr.y<-spr.height || spr.y>render.getHeight())
|| (spr.time_life>0 && spr.time_life<currentTime())
|| (spr.x<-spr.width || spr.x> render.getWidth())
){
this.remove(i);
}
render.drawSprite(this.x_base,this.y_base,spr);
}
}
}
};
List 2.4
With SpriteManager
, we can instance enemy bullets, hero bullets and explosion as SpriteManager
objects:
const MAX_ENEMY_BULLETS=20;
const MAX_HERO_BULLETS=10;
const MAX_EXPLOSIONS=20;
var enemy_bullet;
var hero_bullet;
var explosion;
function init(){
...
enemy_bullet=new SpriteManager(MAX_ENEMY_BULLETS,image[7],0);
hero_bullet=new SpriteManager(MAX_ENEMY_BULLETS,image[9],0);
explosion=new SpriteManager(MAX_EXPLOSIONS,image[8],200);
...
}
List 2.5
On the other hand, enemy sprite manager has a custom implementation from SpriteManager
as each enemy is a sprite with two frames used for animation. Also, enemy SpriteManager
has to implement some functions as doAttack
or check_collision
.
Next, the following code presents a simplified implementation of EnemyManager
from the original source (to see clearly the overrides doAttack
and checkcollision
):
class EnemyManager:SpriteManager{
var time_mov;
var y_top;
function constructor(){
this.sprite=[];
this.x_base=20;
this.y_base=50;
var x=0;
for(var i=0; i < 3; i++){
for(var j=0; j < 15; j++){
var color=0;
var frame1=0;
var frame2=0
if(i == 0){
color = 0x00FF00;
frame1=0;
frame2=1;
}else if(i==1){
color = 0x00FFFF;
frame1=2;
frame2=3;
}else if(i == 2){
color = 0xFFFF00;
frame1=4;
frame2=5;
}
var spr=new CMySprite(100,color);
spr.active=true;
this.sprite.add(spr);
spr.addFrame(image[frame1]);
spr.addFrame(image[frame2]);
spr.setTimeFrame(500);
spr.x=x;
spr.y=18*i;
spr.attack_time=currentTime()+rand()%BASE_TIME_ATTACK+5000;
x+=16;
}
}
}
function checkCollision(spr){
if(spr.active){
for(var i=0;i < hero_bullet.sprite.size(); i++){
if(hero_bullet.sprite[i].active){
if(checkCollision(this.x_base,this.y_base,spr,hero_bullet.sprite[i])){
hero_bullet.remove(i);
spr.active=false;
this.base_mov-=15;
score+=100;
if(top_score<score){
top_score=score;
}
explosion.create(spr.x,spr.y,0,0);
}
}
}
}
}
function doAttack(spr){
if(spr.attack_time>0){
if(spr.attack_time<currentTime()){
spr.attack_time=currentTime()+BASE_TIME_ATTACK+rand()%5000;
enemy_bullet.create(spr.x,spr.y,0,2);
}
}
}
};
List 2.6
In list 2.6, we can see that sprite manager implementation for enemies checks collision for each sprite with hero bullet, when hero bullet collides with some enemy sprites, it adds score and creates explosion at the same position where the enemy died.
In doAttack
function, it implements if the enemy is ready to do an attack that basically creates a bullet at the same position where the enemy is.
Update Function
The update
function is the main update
function that engine calls in each iteration and will update all SpriteManager
objects instanced in the init
function as we can show in the following code:
hero_bullet.update();
explosion.update();
enemy.update();
enemy_bullet.update();
In the same function, we have to know if any enemy bullet collides with hero sprite and then do the respectively actions:
for(var i=0;i < enemy_bullet.sprite.size(); i++){
if(enemy_bullet.sprite[i].active){
if(checkCollision(hero,enemy_bullet.sprite[i])){
enemy_bullet.remove(i);
}
}
}
Furthermore, the update
function will check whether left or right is pressed through T_LEFT
/T_RIGHT
variables already registered in the main.cpp. If one of these keys is pressed, the hero will move to left/right respectively. About hero bullets, if the key SPACE is pressed, a hero bullet will be created at hero position as it shows the following code:
if(TR_LEFT){
hero.x--;
}
if(TR_RIGHT){
hero.x++;
}
if(T_SPACE){
hero_bullet.create(hero.x, hero.y, 0, -5);
}
Conclusion
In this article, we have seen a complete example of application made in C++ as game engine combined with ZetScript that allowed us to bind C++ functions and variables in script side. I would have liked to go more in depth about how the game engine works but it would go out of the main purposes of this article and maybe it would have been a bit tedious to follow it.
Invader game consists of about 800 lines and it would have could be less if SpriteMannager
would have been implemented in the C++ side, but I preferred to show the combination of C++ with ZetScript as mixed as possible to see its benefits.
History
- 2020-09-XX ZetScript Game Engine 2.0.0: Changes since 2.0
- 2018-02-26 Port invader demo to online version, thanks to emscripten
- 2018-02-21 ZetScript Game Engine 1.1.0: Changes since 1.2
- 2017-11-29 ZetScript Game Engine 1.0.0: First release