GBX: Lightweight Scene-Entity game engine for the META!

By ZappedCow, 5 months ago

Howdy,

I have been quite silent since I received my console. As I couldn't figure out which game to make, I decided to try and create game engine for the Gamebuino-META :)

https://github.com/jlauener/GBX

The engine comes as an Arduino library so that it can be installed in the 'libraries' folder. As it's only two files (header and compilation unit) it can easily be copied into a sketch folder and hacked.

It's still very early in development but so far I'm quite satisfied with it. It has been quite difficult to find the right balance between ease of use and efficient RAM usage. But I feel like I'm slowly getting there :) You can check the README on the repo for features and the roadmap.

Here is a demo of a basic breakout game made in GBX. Not a full game though! It is just to show core things such as sprites, animations and collisions. It runs at 61% of CPU (when there are all the blocks) and free RAM is 12495 (it doesn't go down as blocks are destroyed because all allocated memory is cached to prevent fragmentation).



The code for the breakout is (only?) 248 lines. You can easily test it on your device if you put GBX.h and GBX.cpp in the sketch folder. Debug console can be turned on/off using the menu button.


#include "GBX.h"

#define TYPE_BALL 0
#define TYPE_PADDLE 1
#define TYPE_BRICK 2
#define TYPE_WALL 3

//-----------------------------------------------------------------------------
// Paddle
//-----------------------------------------------------------------------------

const uint16_t paddleSpriteData[] =
{
  // width, height
  16, 4,
  // transparent color
  0xF81F,
  // bitmap
  0xF81F, 0xfeb2, 0xfeb2, 0xfeb2, 0xfeb2, 0xfeb2, 0xfeb2, 0xfeb2, 0xfeb2, 0xfeb2, 0xfeb2, 0xfeb2, 0xfeb2, 0xfeb2, 0xfeb2, 0xF81F, 0xfeb2, 0xcc68, 0xcc68, 0xcc68, 0xcc68, 0xcc68, 0xcc68, 0xcc68, 0xcc68, 0xcc68, 0xcc68, 0xcc68, 0xcc68, 0xcc68, 0xcc68, 0xfd42, 0xfeb2, 0xcc68, 0xcc68, 0xcc68, 0xcc68, 0xcc68, 0xcc68, 0xcc68, 0xcc68, 0xcc68, 0xcc68, 0xcc68, 0xcc68, 0xcc68, 0xcc68, 0xfd42, 0xF81F, 0xfd42, 0xfd42, 0xfd42, 0xfd42, 0xfd42, 0xfd42, 0xfd42, 0xfd42, 0xfd42, 0xfd42, 0xfd42, 0xfd42, 0xfd42, 0xfd42, 0xF81F
};

const uint8_t paddleSolidType[] =
{
  // count
  1,
  // entity types
  TYPE_WALL
};

class Paddle : public Entity
{
public:
  Paddle() :
    sprite(paddleSpriteData, -8, -2)
  {
    setHitbox(-8, -2, 16, 4);
  }

  void update()
  {
    if (gbx::isDown(BUTTON_LEFT))
    {
      moveBy(-2, 0, paddleSolidType);
    }
    else if (gbx::isDown(BUTTON_RIGHT))
    {
      moveBy(2, 0, paddleSolidType);
    }
  }

  void draw(int16_t x, int16_t y)
  {
    sprite.draw(x, y);
  }

private:
  Sprite sprite;
};

//-----------------------------------------------------------------------------
// Brick
//-----------------------------------------------------------------------------

const uint16_t brickSpriteData[] =
{
  // width, height
  8, 4,
  // transparent color
  0xF81F,
  // bitmap
  0xca30,0xca30,0xca30,0xca30,0xca30,0xca30,0xca30,0xd8e4,0xca30,0xd8e4,0xd8e4,0xd8e4,0xd8e4,0xd8e4,0xd8e4,0x9008,0xca30,0xd8e4,0xd8e4,0xd8e4,0xd8e4,0xd8e4,0xd8e4,0x9008,0xd8e4,0x9008,0x9008,0x9008,0x9008,0x9008,0x9008,0x9008,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xd8e4,0xd8e4,0xd8e4,0xd8e4,0xd8e4,0xd8e4,0xffff,0xffff,0xd8e4,0xd8e4,0xd8e4,0xd8e4,0xd8e4,0xd8e4,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xd8e4,0xd8e4,0xd8e4,0xd8e4,0xd8e4,0xd8e4,0xd8e4,0xd8e4,0xd8e4,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xd8e4,0xd8e4,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xd8e4,0xd8e4,0xd8e4,0xd8e4,0xd8e4,0xd8e4,0xd8e4,0xd8e4,0xd8e4,0x7ddf,0x7ddf,0x7ddf,0x7ddf,0x7ddf,0x7ddf,0x7ddf,0x4439,0x7ddf,0x4439,0x4439,0x4439,0x4439,0x4439,0x4439,0x210,0x7ddf,0x4439,0x4439,0x4439,0x4439,0x4439,0x4439,0x210,0x4439,0x210,0x210,0x210,0x210,0x210,0x210,0x210,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0x4439,0x4439,0x4439,0x4439,0x4439,0x4439,0xffff,0xffff,0x4439,0x4439,0x4439,0x4439,0x4439,0x4439,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0x4439,0x4439,0x4439,0x4439,0x4439,0x4439,0x4439,0x4439,0x4439,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0x4439,0x4439,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff,0x4439,0x4439,0x4439,0x4439,0x4439,0x4439,0x4439,0x4439,0x4439
};

#define IDLE 0
#define BREAK 1

const uint8_t brickAnimData[] =
{
  // IDLE (frame count, mode, interval, frames...)
  1, LOOP, 0, 0,
  // BREAK (frame count, mode, interval, frames...)
  2, ONE_SHOT, 2, 1, 2
};

#define BRICK_RED 0
#define BRICK_BLUE 1

class Brick : public Entity
{
public:
  Brick() :
    sprite(brickSpriteData, brickAnimData)
  {
    setHitbox(8, 4);
    sprite.play(IDLE);
  }

  void setBrickType(uint8_t brickType)
  {
    sprite.frameOffset = brickType * 3;
  }

  void draw(int16_t x, int16_t y)
  {
    sprite.draw(x, y);
  }

  void hit()
  {
    setFlag(FLAG_COLLIDABLE, false);
    sprite.play(BREAK);
  }

private:
  Anim sprite;
};

//-----------------------------------------------------------------------------
// Ball
//-----------------------------------------------------------------------------

const uint16_t ballSpriteData[] =
{
  // width, height
  4, 4,
  // transparent color
  0xF81F,
  // bitmap
  0xF81F,0xacd0,0xacd0,0xF81F,0xacd0,0xffff,0xffff,0xacd0,0xacd0,0xffff,0xacd0,0xacd0,0xF81F,0xacd0,0xacd0,0xF81F
};

const uint8_t ballSolidType[] =
{
  // count
  3,
  // entity types
  TYPE_PADDLE, TYPE_BRICK, TYPE_WALL
};

class Ball : public Entity
{
public:
  Ball() :
    sprite(ballSpriteData, -2, -2)
  {
    setHitbox(-2, -2, 4, 4);
  }

  void update()
  {
    moveBy(dx, dy, ballSolidType);
  }

  void draw(int16_t x, int16_t y)
  {
    sprite.draw(x, y);
  }

protected:
  bool onMoveCollideX(Entity& other)
  {
    if (other.getType() == TYPE_BRICK)
    {
      Brick& brick = (Brick&)other;
      brick.hit();
    }
    dx = -dx;
    return true;
  }

  bool onMoveCollideY(Entity& other)
  {
    if (other.getType() == TYPE_BRICK)
    {
      Brick& brick = (Brick&)other;
      brick.hit();
    }
    dy = -dy;
    return true;
  }

private:
  int16_t dx = 1;
  int16_t dy = -1;
  Sprite sprite;
};

//-----------------------------------------------------------------------------
// GameScene
//-----------------------------------------------------------------------------

class GameScene : public Scene
{
public:
  GameScene() :
    ball(this, TYPE_BALL, 1),
    paddle(this, TYPE_PADDLE, 1),
    bricks(this, TYPE_BRICK, 48),
    walls(this, TYPE_WALL, 4)
  {
  }

  void onInit()
  {
    walls.spawn()->setHitbox(-8, 0, 8, (uint8_t) gbx::height); // left wall
    walls.spawn()->setHitbox((int8_t) gbx::width, 0, 8, (uint8_t) gbx::height); // right wall
    walls.spawn()->setHitbox(0, -8, (uint8_t) gbx::width, 8); // top wall
    walls.spawn()->setHitbox(0, (int8_t) gbx::height, (uint8_t) gbx::width, 8); // bottom wall

    ball.spawn(gbx::width / 2, gbx::height - 16);

    paddle.spawn(gbx::width / 2, gbx::height - 8);

    bool flipFlop = false;
    for (int16_t ix = 0; ix < 8; ix++)
    {
      for (int16_t iy = 0; iy < 6; iy++)
      {
        bricks.spawn(8 + ix * 8, 8 + iy * 4)->setBrickType(flipFlop ? BRICK_RED : BRICK_BLUE);
        flipFlop = !flipFlop;
      }
      flipFlop = !flipFlop;
    }
  }

private:
  EntityPool<Ball> ball;
  EntityPool<Paddle> paddle;
  EntityPool<Brick> bricks;
  EntityPool<Entity> walls;
};

//-----------------------------------------------------------------------------
// Main
//-----------------------------------------------------------------------------

GameScene gameScene;

void setup()
{
  gbx::init();
  gbx::setScene(gameScene);
}

void loop()
{
  gbx::update();
}


Hope someone finds it useful.

Last comments

jicehel

NEW 5 months ago

Yes, it was the idea. Undestand that are the benefits of GBX and you have well explain it. It manage a physical motor too ? 

Seems interesting and i hope you will make some little tutos for beginners as it can let us (the beginners) making games more easy with some change of programming method. Thanks for your informations and i'll check the link too.

ZappedCow

NEW 5 months ago

Hello, first of all sorry for the lack of documentation. Actually the only documentation is the example breakout game I posted.

GBX is a game engine or at least it tries to be. The GameBuino-Meta is a framework for creating multimedia application (like SDL2) so lot of boilerplate code is needed to write an actual game.

At it's core GBX provides an entity system (https://en.wikipedia.org/wiki/Entity%E2%80%93component%E2%80%93system). The engine let you create entities by extending the Entity base class. Then you give those entities to the engine and he will manage them (keep the memory, call update()/draw() each loop, ..) for you. On top of the entity system, GBX provides some features such as collision mamangement, debug console and animations. I think most game engines work like that.

The main motivation behind writting GBX is that I already wrote an Arduboy game (CastleBoy). Now I want to write Gambuino games. Maybe tomorrow I want to write XXX games. I also wrote lot of PC games. And everytime I rewrite the same entity and collision system. Everytime.

Now I could have kept in on my private repo. But I think it could be useful for other fellows here.

You don't have to use it. You can use it if you wish to. I don't force anyone to use GBX ;)

jicehel

NEW 5 months ago

Hi and sorry i haven't commented yet. Why ? Only because i haven't yet understand how to use GBX and why i have to use it. What it's help to do, asw. Maybe you have already done a doc or a website to explain how to use and what it's possible to do with it that i couldn't do or what i can do easier with GBX but atm i don't know yet and i just see some more syntax to learn. With your explanation of the interests i'll probably more understand why i have to use it to do better games or program them easiest.

Quality seal
Quality seal