suntdoar.eu

Punity, a Unity-like Game Engine for the Raspberry Pi Pico

Written on

A picture of my game.

A picture of my game.

I had the task of creating a game in C++ for my University’s Object Oriented Programming class. Students usually build their game on top of the professor’s template, but I wanted to do something different.

I wanted to create a game engine for a microcontroller. One that would be easy to use but could be extended to allow for developing more complex games.

In this article I will explain the step-by-step process of my game engine development process, all of it cumulating to me winning a prize from Amazon and getting first place at the University’s Student Scientific Communications Session.

I will also give some important insight that students should know about based on my experience as a student.

Goals

My goals were simple:

  1. Create a game engine that would be easy to use and extend (like Unity).
  2. Make it run on a Raspberry Pi Pico.
  3. Create a game using the engine to showcase its capabilities.

A picture of the Raspberry Pi Pico.

A picture of the Raspberry Pi Pico.

The Raspberry Pi Pico has a 133 MHz dual-core processor and 264 KB of RAM. Optimizing the game engine to run on such a device was a challenge, but I was up for it.

Game engine design: Unity as a case study

I had some experience with Unity and I liked how easy it was to create a game with it.

In Unity, your building blocks are GameObjects. You can attach Components to them, which in essence are scripts that can be used to add functionality to the GameObject.

I feel like the best way to get a grasp of how Unity works is to show you a simple script:

using UnityEngine;

/*
*   Example script that moves the GameObject 
*   forward at a constant speed.
*/

// All scripts must inherit from MonoBehaviour
public class MoveObject : MonoBehaviour
{
    public float speed = 10f; // Speed in units per second

    // Transform is a component that holds the position,
    // rotation and scale of the GameObject
    private Transform objectTransform;

    // Start is called before the first frame update
    private void Start()
    {
        objectTransform = GetComponent<Transform>();
    }

    // Update is called once per frame
    private void Update()
    {
        // Move the GameObject
        objectTransform.Translate(
            Vector3.forward * speed * Time.deltaTime
        );
    }
}

This script can be attached to any GameObject and it will move it forward at a constant speed.

Unity's Rigidbody component attached to a GameObject.

Unity’s Rigidbody component attached to a GameObject.

Another thing you should know is that GameObjects form a tree structure. This means that a GameObject can have children GameObjects.

Nested GameObjects.

Nested GameObjects.

Basically, Unity lets you treat GameObjects as if they were directories in a file system, while Components are the files. For example, deleting a GameObject will delete all of its children GameObjects and their Components.

I wanted this kind of behavior in my game engine. It felt natural and easy to use.

Obviously, I couldn’t use Unity itself for the Raspberry Pi Pico. Even an empty Unity game would take more than the RAM of the Pico. That’s where my engine, Punity, will come in.

So I started working on it.

The first iteration

A lesson that I learned immediately was that you should never start a project by writing code. You should always start by writing down your ideas and planning your project.

I’m telling you this because my first attempt had to be scrapped. I didn’t plan correctly and ended up creating something that I didn’t want to code a game with.

However, I made progress:

I wrote a really fast driver for my display.

A picture of my display showcasing its power.

A picture of my display showcasing its power.

I achieved this speed by coming up with something similar to a double buffer. I had two buffers, one that represented the last frame and one that represented the new frame. I then calculated the difference between the two and only updated the pixels that changed!

The change matrix. Elements which are 1 are different, while elements which are 0 are the same.

The change matrix. Elements which are 1 are different, while elements which are 0 are the same.

This simple optimization made my game run at 60 frames per second with no flickering.

I also wrote a driver for the joystick!

A picture of my joystick driver. The potentiometer had a problem.

A picture of my joystick driver. The potentiometer had a problem.

The joystick would be the main input for my game engine, besides buttons.

The biggest mistake I was that I didn’t think I needed the child-parent relationship that Unity had. I thought a list of separate GameObjects would suffice. But in reality, it was a pain to work with.

In a nutshell, I wrote code without a plan. Don’t do that.

I put an end to this first game engine iteration right after I also implemented collision. I wanted to make a simple map and… I was beginning to feel how inconvenient it was to work with the inheritance-based Components.

Collision working.

Collision working.

Conclusion? Plan before you code.

So many students make this mistake and I made it too.

I had to start over.

A new beginning

First things first, I worked on behaviour. Using composition, I came up with the complete definition of an Entity (which is the equivalent of a GameObject in Unity):

The complete GameObject structure (named *Entity* in my paper).

The complete GameObject structure (named Entity in my paper).

At the C++ level, this is how it looks like:

 class PEntity {
    private:
        // Entity name
        std::string m_name;

        // Children entities
        std::list<Punity::PEntity*> m_children_entities;

        // List of components
        std::list<Punity::Components::PComponent*> m_components;

        static uint64_t entity_count;
        uint64_t m_id = entity_count++; // Increment id's

        // Parent entity
        PEntity* m_parent_entity = nullptr;
        
        // [...] A LOT more functions
 };

Basically, Components would be attached to GameObjects, just like in Unity. The user would override the default events for the Components to add functionality to them.

Each Entity had to have a Transform component which would hold the position of the Entity. This was the only Component that was mandatory.

My Component interface provided you with these events:

Function When it’s called
void on_enable() Each time the component is enabled.
void on_disable() Each time the component is disabled.
void on_destroy() When the component is destroyed.
void on_start() When the engine starts.
void on_update() Each frame.
void on_start_collision(PCollider*) When you first touch another collidable.
void on_collision(PCollider*) Each frame you touch another collidable.
void on_end_collision(PCollider*) When you stop touching another collidable.

Table of Component events. The PCollider parameter is the collider your Entity is touching.

Using these concepts, here is an example component that would move the GameObject up at a constant speed:

class AlwaysMoveUp : public Component {
    void on_start_collision(Collider* other) override {
        // Get parent entity of component
        Entity* parent_entity = this>get_entity();

        // Get Transform component of entity
        Transform* entity_transform = 
            parent_entity>get_component<Transform>();

        // Translate entity by 10 pixels on the Y axis
        entity_transform>translate({0, 10});
    }
};

No, this is not Unity, this is Punity!

Both Unity and Punity use getComponent on entities and rely on the provided Component Interface to work. This is a good thing because it allows for polymorphism and dynamic dispatch of events.

Moving on, I had to let Transform propagate its changes in position to children of the Entity it’s attached to. I attached a blue square to a black square and only moved the black square. This was the result:

The parent-child relationship between GameObjects.

The parent-child relationship between GameObjects.

As you can see, the blue square is following the black square!

I’ve also created a way to have read-only variables in C++ using references to constants just as a quality of life feature. For example, the Transform class had these variables:

private:
    // The local position will be relative to the parent object
    // Basically an offset
    Utils::PVector m_local_position;
    // The global position will be relative to the root. 
    // The root is standing at 0, 0.
    Utils::PVector m_global_position;
public:
    Utils::PVector const& local_position = m_local_position;
    Utils::PVector const& global_position = m_global_position;

Anyone could read Transform.local_position, but you couldn’t set it because the compiler would not allow setting a value to a reference of a constant variable, even if the variable itself is not constant.

All in all, this was amazing. It felt like I was going to make it this time.

Composition truly changed the whole infrastructure of my game engine. Yeah, for me, it was kind of a perspective shift.

You know, I’ve been writing code for several years now, but my past experience consists mainly of algorithms, data structures and competitive programming. Because of that, I never really had to think about how to structure my code or how to make it easy to use. I just had to make it work, fast.

It’s an issue prevalent with lots of competitive programmers and for the past year or so I’ve been working to fix it. I’ve been learning about software design and architecture, and I’ve been trying to apply it to my projects. Punity was the biggest so far, and it taught me a lot.

Going back to the engine, the next step was the most painful.

The painful part

Everything was dynamic. Because of that and because I didn’t know about smart pointers, I had to manually manage memory. Sometimes, memory leaks would occur and I would have to spend hours debugging them.

Average memory corruption POV

Average memory corruption POV

I was tested. I was tested to see if I really wanted to do this.

You’d think something like:

Come on, just use the debugger, it’s not that hard.

I couldn’t get the debugger to work with the Raspberry Pi Pico. It’s not just plug-and-play, mind you. You need to have a second Raspberry Pi to act as a debugger. I didn’t have one at first and got one for this specific reason, but I couldn’t get it to work probably because of my wiring being weak or something.

So I had to debug the memory leaks without a debugger and without any error messages. Only print statements.

One error which was freeing something twice in a random destructor took me around 12 hours to figure it out. After 12 hours or so of debugging, it felt like I gained a new superpower and nothing would stop me anymore.

Starting to look like a game engine

I went on and created a mini-library of Components, enough for anyone to create a simple game.

Sometimes they worked well,

Sprites working.

Sprites working.

sometimes they didn’t.

Sprites NOT working.

Sprites NOT working.

This was the resulting “library”:

The Punity library.

The Punity library.

You could expand the collection of Colliders by extending PCollider. I created a PCircleCollider and a PBoxCollider for my game.

This library was more than enough to create a simple game. You could:

  • Use sprites with layers and transparency
  • Have collision
  • Group GameObjects in a tree structure
  • Attach your own Components to GameObjects

Let’s now check out how Punity code looks!

This is an example of how to create GameObjects and attach Components to them. This is actual code I use for creating enemies in my game:

Punity::PEntity* make_enemy(Punity::PEntity* parent, uint8_t type) {
    auto enemy_entity = Punity::PEntity::make_entity(Game::Names::ENEMY, parent, true);

    // Choose sprite
    enemy_entity->add_component<Punity::Components::PSpriteRenderer>()->set_sprite(
            SPRITE(Game::Sprites::first_enemy_type, Game::Sprites::Layers::PLAYER)
    );

    // Set the collider
    enemy_entity->add_component<Punity::Components::PCircleCollider>()
            ->set_radius(Game::Sprites::first_enemy_type_h / 2)
            ->set_information(Game::Colliders::ENEMY);

    // Set enemy behaviour
    enemy_entity->add_component<EnemyBehaviour>();
    
    // Entity for selector that appears 
    // above enemy when the player aims at it
    auto selector_entity = Punity::PEntity::make_entity(Game::Names::SELECTOR, enemy_entity, false);
    
    // Place the selector in offset regarding enemy
    selector_entity->get_transform()->set_local({0, -8}); 
    selector_entity->add_component<Punity::Components::PSpriteRenderer>()->set_sprite(
            SPRITE(Game::Sprites::enemy_selected_arrow, Game::Sprites::Layers::SELECTOR)
    );

    return enemy_entity;
}

Enemy entity creation code (Unity Prefab wannabe)

Looks good enough—It’s time for game development!

The game

The idea was simple:

  1. You enter a room and kill some enemies.
  2. Your guns use energy and you have limited hitpoints.
  3. You can recharge your energy and heal yourself by picking up stuff from enemies.
  4. After finishing the enemies, get a random reward (maybe a weapon).
  5. Go to the next room until you hit level 3.
  6. After level 3, you win!

I called the game “Pain” because of the pain I went through to make it. Yeah, edgy.

The structuring of the entities looked more or less like this:

The entity structure of the game.

The entity structure of the game.

It enabled easy handling of enemies and players. I used the top-level World entity to handle game logic and manage the lower-level entities, for example.

This was the power of the tree structure. Easy handling and management of entities.

The sprites were loaded by running a Python script which generated C++ header files with arrays of bytes:

uint16_t const player_bullet_1 [3 * 3] = { 0x0, 0x3a59, 0x0, 0x3a59, 0x9edd, 0x3a59, 0x0, 0x3a59, 0x0 };
bool const player_bullet_1_alpha [3 * 3] = { false, true, false, true, true, true, false, true, false };
uint16_t const player_bullet_1_w = 3;
uint16_t const player_bullet_1_h = 3;

They were all encoded with RGB565 since the screen uses that format.

Simple maths were used for collision detection and aiming. The game even featured a way to detect whether the player could hit an enemy or not by using a raycast.

Enemies used some random pathing algorithm. They were good enough at shooting me and made the game difficult enough.

A picture of the game. An enemy is shooting.

A picture of the game. An enemy is shooting.

The game was coming together. I had a lot of fun developing it and I was really proud of what I achieved.

Me playing my game.

Me playing my game.

However, the hard part was yet to come. I decided to enroll in the student contest at my university. I had to write a paper and present it in front of a jury. I had to explain my game engine and my game.

The presentation

Now this is something I’d like to tell every student out there:

Learn to market yourself.

Seriously.

I’ve seen so many students with amazing projects that they don’t know how to present. They don’t know how to sell themselves. They don’t know how to make people interested in what they did.

As a reference, I was the only year 2 student in my category. The other students were year 4. I was the youngest and I got first place.

Why?

Because I knew how to present my work. I created a good slideshow and focused on connecting with the jury. I made sure they understood what I did and why I did it.

I don’t think I’m that good of a writer, but to talk 20 minutes and the first thing the jury asks you is what is your project about… that’s unspeakable.

When it came to my turn, I was concise, I used simple terms and presented in various tones without missing too much detail. Slides were almost word-free and I made sure to explain everything in my own words.

This helped immensely. It felt like I revived the jury.

A few hours later, I was declared winner.

Conclusion

It really was a journey developing this game engine for the past months. I learned about software design, architecture, marketing and presentation. I connected with people I never thought I would connect with. I learned a lot about myself and my capabilities.

It wasn’t easy. It wasn’t easy at all. I had to sacrifice most of my free time for this.

But it was worth it. This is how you evolve and this is how you truly become better at what you do.

I hope this inspires you to go further,

Extremq.

Greetings! I am at the beginning of my blogging journey. If you like my writing, please support me by leaving a star on this project.