{"id":22415555,"url":"https://github.com/extremq/punity","last_synced_at":"2025-08-01T00:32:03.530Z","repository":{"id":63256241,"uuid":"564876278","full_name":"extremq/punity","owner":"extremq","description":"Unity inspired game engine for the Raspberry Pi PIco.","archived":false,"fork":false,"pushed_at":"2023-12-31T10:36:02.000Z","size":462,"stargazers_count":8,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-11-29T08:13:40.400Z","etag":null,"topics":["2d-game-engine","cpp","ecs","game-development","game-engine","gamedev","microcontrollers","picosdk","raspberry-pi"],"latest_commit_sha":null,"homepage":"","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/extremq.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2022-11-11T17:58:55.000Z","updated_at":"2024-09-16T17:02:50.000Z","dependencies_parsed_at":"2023-12-31T11:27:21.077Z","dependency_job_id":"95355c8d-d34c-41a2-807c-96d29924bcb0","html_url":"https://github.com/extremq/punity","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/extremq%2Fpunity","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/extremq%2Fpunity/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/extremq%2Fpunity/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/extremq%2Fpunity/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/extremq","download_url":"https://codeload.github.com/extremq/punity/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":228321274,"owners_count":17901604,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["2d-game-engine","cpp","ecs","game-development","game-engine","gamedev","microcontrollers","picosdk","raspberry-pi"],"created_at":"2024-12-05T15:12:43.701Z","updated_at":"2024-12-05T15:12:44.302Z","avatar_url":"https://github.com/extremq.png","language":"C++","readme":"\n\n# Punity\n![](https://i.imgur.com/e5t4SCO.png)\n\u003cdetails\u003e\u003csummary\u003eMore pictures!\u003c/summary\u003e\n\nhttps://github.com/extremq/punity/assets/45830561/67613c82-e012-4bbc-872c-dfe4c5c9e1e0\n\n![](https://i.imgur.com/ER29NXM.jpg)\n\n![](https://i.imgur.com/I4rq7I1.jpg)\n\n![](https://i.imgur.com/vKiKM2b.png)\n\u003c/details\u003e\n\nPunity (*noun: punishment, punitiveness*) is \na 2D game engine targeted to the Raspberry Pi Pico microcontroller, designed with Unity as an inspiration.\n\nIn this implementation, the Screen is using a ST7735 controller. You will also find logic for interfacing analog joysticks and buttons.\n\n### Extra info\n**I had no access to a debugger, the ram of the Pico is limited to\n264KB, and it has a 100MHz CPU.**\n\nThis project has been made for a university task.\n\nCheck out [my previous attempt](https://github.com/extremq/pico_game/) at creating such a game engine. I've learnt from that and moved on to creating Punity just a few days later.\n\n# Documentation\nPlease note this isn't an exhaustive documentation. A lot has been omitted. Check the implementations,\nthey are always commented and easy to understand if you want more details. Also check out\nthe game for example usage.\n### 1. The structure\nThe Punity system is similar in nature to Unity. If you've never heard of Unity before, it's a hierarchical game engine that is widely used today.\n\nA hierarchical game engine employs entities and components that reside in memory the same way files and folders do:\n![](https://i.imgur.com/PNicDNp.png)\nLeft, you have folders and subfolders with files. Right, you have my entity structure that resembles folders and subfolders.\n\nThis way of handlind entities makes it so when disabling or destroying an entity, you also destroy all their children entities. Think of how deleting a folder deletes every subfolder.\n\nYou can easily disable and enable the player_normal without needing to disable their sword, their arms, hats, etc. since eveything is a child of the player_normal.\n\nEach entity also has a component. Components simply extend an entity's behaviour. You could make a Sprite component which attaches a sprite to the entity, so it can be rendered on the screen, or you could add a BoxCollider component, so you make it have a rectangular hitbox.\n\nEach entity will have a Transform component, which describes the position of the entity in the world.\n\n### 2. The Engine\nNow that we have a structure, I need to talk about how the engine works. Basically, logic happens at component level - that means something acts, something has a behaviour because of components. Entities themselves are empty shells with no behaviour.\n\nThe way you add functionality to components is by creating overriding their abstract functions. These are:\n\n|               Function               | When it's called                           |\n|:------------------------------------:|:-------------------------------------------|\n|           void on_enable()           | Each time the component is enabled.        |\n|          void on_disable()           | Each time the component is disabled.       |\n|          void on_destroy()           | When the component is destroyed.           |\n|           void on_start()            | When the engine starts.                    |\n|           void on_update()           | Each frame.                                |\n| void on_start_collision(PCollider\\*) | When you first touch another collidable.   |\n|    void on_collision(PCollider\\*)    | Each frame you touch another collidable.   |\n|  void on_end_collision(PCollider\\*)  | When you stop touching another collidable. |\n\nNow, all you have to do to make an entity print something is simple:\n1. Make a class `MyComponent` that inherits `PComponent` publicly.\n2. Create an entity using `auto my_entity = Punity::Pentity.make_entity(name, is_active)`.\n3. Add the component using `my_entity.add_component\u003cMyComponent\u003e()`.\n4. That's it!\n\n```cpp\nclass MyComponent : public Punity::Components::PComponent {\n    void on_update() override {\n        std::cout \u003c\u003c \"Hello!\\n\"; // Prints 'Hello!' every frame.\n    }\n};\n\n// Somewhere else in your code\nauto my_entity = Punity::Pentity.make_entity(\"My_entity\", true);\nmy_entity.add_component\u003cMyComponent\u003e();\n```\n\nThe engine is memory managed automatically. Other than that, anything else you allocate\nresources for you are responsible for handling. Don't use `delete` on entities\nor components.\n\nGoing into the details, this is a bit more complicated. \nSince my engine supports disabling, deleting, and whatnot, there are many edge cases\nthat need to be treated.\n\nMy engine has a list of entities that it goes through each frame.\n\nFirst of all, which entity is called first? Is it the children or the parent?\nWhat happens if I destroy the parent of a child and then call something within the child?\n\nTo resolve this, I've implemented a clear **Order of Execution**.\nIt goes like this:\n1. First, check which entities are disabled/destroyed and remove them from the active entities list\n2. Then, safely delete the destroyed entities\n3. New frame starts here\n4. Load the background\n5. Loop through all entities and update them while also collecting colliders and sprites\n6. Solve Invokers\n7. Compute collisions\n8. Draw sprites\n9. Send the frame\n10. Sleep the remaining time and repeat\n\nThis means that deletion isn't instant. It takes effect the next frame. It's best to\ncheck the source code if you want to dig even deeper. `punity/Engine/PEngine.cpp`\n\n### 3. Punity Utils\n#### 3.1. Invokable\nLet's say you want to show enable an entity after a delay.\nYou could use ifs and whatever, but I made something just for you!\n\nInvokables are a way of calling a function after a specified time in seconds.\n\nThe syntax is as follows:\n```cpp\nnew Punity::Utils::PInvokable\u003cComponent\u003e(\n        \u0026Component::function,\n        this,\n        delay,\n        get_entity()-\u003eget_id()\n        );\n```\n\nNote that the function must be of type `void(void)`.\n\nI also support function with an int parameter using PInvokableWithInt.\n\n### 3.2. Time\nYou can get the elapsed time in second since boot easily using the `Punity::Time.time` util.\n\nIf you'd like to make physics computation independent of frame rate, you can also use\n`Punity::Time.delta_time`, which gives you the difference in time between the frames.\n\nHere is a snippet that implements player_normal movement using a joystick.\n\n```cpp\n// Construct vector of direction\nauto joystick_direction = Punity::Utils::PVector();\njoystick_direction.x = Punity::Joystick.get_x_direction();\njoystick_direction.y = Punity::Joystick.get_y_direction();\n\n// Normalize the vector\njoystick_direction = joystick_direction.norm();\n\n// Save the last direction\nlast_joystick_direction = joystick_direction;\n\n// Translate the player_normal\nget_entity()-\u003eget_transform()-\u003etranslate(joystick_direction * Punity::Time.delta_time * 30);\n```\n\n### 3.3. Vector\n`Punity::Utils::PVector` is a 2D vector with built-in support for popular operations.\nHere is a snippet from the collision computation util which uses `PVector`.\n```cpp\n// Mathematics\nnearest_point.x = std::max(rect_pos.x - rect-\u003ewidth / 2, std::min(circle_pos.x, rect_pos.x + rect-\u003ewidth / 2));\nnearest_point.y = std::max(rect_pos.y - rect-\u003eheight / 2, std::min(circle_pos.y, rect_pos.y + rect-\u003eheight / 2));\n\n// Ray to the nearest point\nUtils::PVector ray_to_nearest_point = nearest_point - circle_pos;\n\nconst float mag = ray_to_nearest_point.mag();\nconst float overlap = circle-\u003eradius - mag;\n\nif (overlap \u003c -1e-5) return false;\n\n// If is a trigger return before modifying position of circle\nif (rect-\u003eis_trigger || circle-\u003eis_trigger) return true;\n\n// Resolution\ncircle_pos.x -= ray_to_nearest_point.x / mag * overlap;\ncircle_pos.y -= ray_to_nearest_point.y / mag * overlap;\n```\n\n### 3.4. Random\n**Disclaimer**, this random number generator isn't a top-notch one, but it's good enough.\nIt uses the ring oscillator provided with the Raspberry Pi Pico.\n\n`Punity::Utils::random(a, b)` or just no parameters generates a number between [a, b] or [0, 1].\n\nHere's an example snippet:\n```cpp\n// Set a random offset.\npause_time = Punity::Utils::random(1.0f, 1.5f);\nshooting_start = Punity::Time.time + Punity::Utils::random() + pause_time;\n```\n\n### 3.5. Math\nUseful math functions such as distance and lerp. They take Vectors as input, same as \nany other implementation on earth.\n\n### 3.6. Collision Computation\nShouldn't be called but need to be implemented if you want to extend shape interactions.\n\n### 4. Input\nConfiguring a button or a joystick is as simple as doing:\n```cpp\nPunity::Joystick.config(JOY_X, JOY_Y); // ADC Pins\nPunity::Button.config(JOY_BTN); // GPIO Pins\nPunity::Button.config(ACTION_BTN);\n```\nTo read values, simply do:\n```cpp\nPunity::Joystick.get_x_direction();\nPunity::Button.read_button(JOY_BTN);\n```\n\n### 5. Screen\nThe screen is managed in such a way that it only transmits the changes in frame.\nI do this because updating the whole frame each time takes a really long time on this screen.\n\nThe screen automatically draws sprites from entities that have `PSpriteRenderer` or `PUISpriteRenderer`\nand layers them according to the assigned layer.\n\nYou can also use a 8x8 sprite to tile the background with.\n\n### 6. Components\nThere are Sprite Components, Collider components and Transform.\nA good snippet showing usage of them all plus other components:\n```cpp\nPunity::PEntity* make_enemy(Punity::PEntity* parent, uint8_t type) {\n    auto enemy_entity = Punity::PEntity::make_entity(Game::Names::ENEMY, parent, true);\n\n    // Choose sprite\n    enemy_entity-\u003eadd_component\u003cPunity::Components::PSpriteRenderer\u003e()-\u003eset_sprite(\n            SPRITE(Game::Sprites::first_enemy_type, Game::Sprites::Layers::PLAYER)\n    );\n\n    // Set the collider\n    enemy_entity-\u003eadd_component\u003cPunity::Components::PCircleCollider\u003e()\n            -\u003eset_radius(Game::Sprites::first_enemy_type_h / 2)\n            -\u003eset_information(Game::Colliders::ENEMY);\n\n    // Set enemy behaviour\n    enemy_entity-\u003eadd_component\u003cEnemyBehaviour\u003e();\n    \n    // Entity for selector that appears above enemy when player_normal aims at\n    auto selector_entity = Punity::PEntity::make_entity(Game::Names::SELECTOR, enemy_entity, false);\n\n    selector_entity-\u003eget_transform()-\u003eset_local({0, -8}); // place the selector in offset regarding enemy\n    selector_entity-\u003eadd_component\u003cPunity::Components::PSpriteRenderer\u003e()-\u003eset_sprite(\n            SPRITE(Game::Sprites::enemy_selected_arrow, Game::Sprites::Layers::SELECTOR)\n    );\n\n    return enemy_entity;\n}\n```\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fextremq%2Fpunity","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fextremq%2Fpunity","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fextremq%2Fpunity/lists"}