-
Notifications
You must be signed in to change notification settings - Fork 117
FlipperZero Game Engine (Video Game Engine)
The FlipperZero Game Engine is a git submodule that you can include in your own Flipper Zero application. Because it is a sub-module, you should clone applications that use the engine using the git clone --recursive option (or --recurse-submodules).
The engine is located at https://github.com/flipperdevices/flipperzero-game-engine and typically people create a submodule in an engine directory.
The game engine contains two main parts:
- Game engine for playing games
-
Gamehas a bunch ofLevel* objects; described byLevelBehaviour -
Levelhas a bunch ofEntity* objects (e.g. player, ball, coin, wall, etc. entity); described byEntityDescription -
Entitycan have a rectangular or circular collider -
Sprite(.png transformed to .fxbm image) can be loaded and rendered to Canvas -
Vector(x & y position) and APIs for manipulating the vector -
GameManagerfor gettingInputState, switchingLevel, loadingSpriteobjects, showing frames-per-second, overall game context, etc.
-
- Sensor library for accessing the Video Game Module
- Pitch : Assuming screen facing up & IR port facing left, then pitch is the tilt Left(+) / Right(-)
- Roll : Assuming screen facing up & IR port facing left, then roll is the GPIO ports Down(+) / GPIO ports Up(-)
- Yaw : Assuming screen facing up & IR port facing left, then yaw is Counter-Clockwise(+) / Clockwise(-)
The game engine was used by the Air Arkanoid game.
I wrote the Air Labyrinth game using the game engine.
JBlanked wrote Flip World game using the game engine.
There is also an example game using the game engine. This example only has one level and does not leverage the Video Game Module for motion.
To add the submodule to your project, you typically run the following command in the same directory as your application.fam file.
git submodule add https://github.com/flipperdevices/flipperzero-game-engine.git engine
If you have a Video Game Module (VGM) attached to your Flipper Zero you can access the orientation in your application. You need all of the code under the sensors folder, but it has no other dependencies.
Your application will need to #include "sensors/imu.h"
You can then call: Imu* imu = imu_alloc(); to obtain an Imu* object. When you call this method, it will try to acquire the IMU connected to the VGM (using pins 2-7, plus power [3v3] and GND). If it detects the module, it will also do a calibration. For best results, you should have the device in the expected orientation and not moving during this time.
Calling bool imu_present = imu_present(imu); will return true if the IMU was present during the allocation call, otherwise it will return false. NOTE: If you are running custom firmware on the Video Game Module, it is possible that the onboard PI may be using the IMU and will be unavailable to the Flipper Zero.
To get orientation data...
float pitch = imu_pitch_get(imu);
float roll = imu_roll_get(imu);
float yaw = imu_yaw_get(img);When you are done with the IMU, you should release the resource using imu_free(imu);.
Create a submodule for the engine. See Adding to your project for commands.
The engine\main.c file defines an entry point of game_app. Your application.fam file should set the entry point using entry_point="game_app",.
You application should #include "engine/engine.h"
The engine\engine.h declares extern const Game game; which will get executed by the game engine. You must define the game variable in your game, setting all of the properties.
The engine\engine.h defines the Game structure as:
typedef struct {
float target_fps;
bool show_fps;
bool always_backlight;
void (*start)(GameManager* game_manager, void* context);
void (*stop)(void* context);
size_t context_size;
} Game;-
target_fps: The number of frames per second to try to render. A value of 30 is probably a good starting point.
-
show_fps: Useful for debugging to see the actual frames per second. Typically set to false.
-
always_backlight: Set to true so the screen is always visible while playing the game.
-
start: Callback that should add the levels & allocate Imu* object
-
stop: Callback that should free Imu* object
-
context_size: Set tosizeof(your context structure). The context will be allocated for you and passed to yourstartandstopmethods.
The Air Arkanoid game uses the following in their game.c file:
void game_start(GameManager* game_manager, void* ctx);
void game_stop(void* ctx);
// The various levels that are created.
typedef struct {
Level* menu;
Level* settings;
Level* game;
Level* message;
} Levels;
// Settings that are saved on SD card.
typedef struct {
bool sound;
bool show_fps;
} Settings;
// Game (App) related data
typedef struct {
Imu* imu; // VGM data
bool imu_present; // cache of imu_present(imu)
Levels levels; // All of the levels in the game
Settings settings; // Settings associated with game (like sound)
NotificationApp* app; // For sound/vibrate
GameManager* game_manager; // Used to set showing/hiding fps info
} GameContext;
const Game game = {
.target_fps = 30, // Update screen 30 frames per second
.show_fps = false, // Don't display the current frames per second
.always_backlight = true, // Keep screen on so it is easy to see
.start = game_start, // Callback that should add the levels & allocate Imu* object
.stop = game_stop, // Callback that should free Imu* object
.context_size = sizeof(GameContext), // Context for storing Game related data.
};Your game_start will allocate the Imu* (if you have motion), allocate NotificationApp* (if you have sound) and will use the GameManager to add levels. If you have game settings on the SD card, it should also load and apply those settings.
Your game_end will free the Imu* object and release the NotificationApp.
When your application did #include "engine/engine.h" that also included the engine/level.h file.
Your game_start callback add levels using the GameManager. It calls the following function to create a new Level* object: Level* game_manager_add_level(GameManager* manager, const LevelBehaviour* behaviour) function.
The engine\level.h defines the LevelBehaviour structure as:
typedef struct {
void (*alloc)(Level* level, GameManager* manager, void* context);
void (*free)(Level* level, GameManager* manager, void* context);
void (*start)(Level* level, GameManager* manager, void* context);
void (*stop)(Level* level, GameManager* manager, void* context);
size_t context_size;
} LevelBehaviour;-
alloc: Callback to allocate the level. Invoked when a level is added to the game manager (at game_start). Often Entity objects are added to the level in theallocfunction, but sometimes they are added in thestartfunction instead.
-
free: Callback to free level. Invoked when game manager is shutting down.
-
start: Callback when a level is started. Invoked when a level is switched to (or the first level added to the game manager). Usually, entity objects have their position set; but some games will also spawn (create) new entity objects in this function.
-
stop: Callback when a level is ended. Invoked when a level is switched away (or game manager is stopping). For games that spawn entity in thestartfunction, thestopfunction often callslevel_clearto remove all Entity objects.
-
context_size: Set tosizeof(your context structure). The context will be allocated for you and passed to your methods.
The Air Arkanoid game uses the following in their levels/level_game.c file:
const LevelBehaviour level_game = {
.alloc = NULL,
.free = NULL,
.start = level_game_start,
.stop = level_game_stop,
.context_size = 0,
};In the above example (level_game), all of the Entity objects are created in level_game_start. The level is cleared in level_game_stop. This is probably because many of the Entity objects are removed as the game is played.
However, in the levels/level_menu.c file the Entity objects are created in level_menu_alloc. The position of the Entity objects are reset in level_menu_start. In the menu, none of the entity are ever destroyed, they are just animated.
const LevelBehaviour level_menu = {
.alloc = level_menu_alloc,
.free = NULL,
.start = level_menu_start,
.stop = NULL,
.context_size = sizeof(LevelMenuContext),
};When your application did #include "engine/engine.h" that also included the engine/entity.h file.
Your level_alloc or level_start callback add entity using the Level. It calls the following function to create a new Entity* object: Entity* level_add_entity(Level* level, const EntityDescription* behaviour) function.
The engine\entity.h defines the EntityDescription structure as:
typedef struct {
void (*start)(Entity* self, GameManager* manager, void* context);
void (*stop)(Entity* self, GameManager* manager, void* context);
void (*update)(Entity* self, GameManager* manager, void* context);
void (*render)(Entity* self, GameManager* manager, Canvas* canvas, void* context);
void (*collision)(Entity* self, Entity* other, GameManager* manager, void* context);
void (*event)(Entity* self, GameManager* manager, EntityEvent event, void* context);
size_t context_size;
} EntityDescription;-
start: Callback when an entity is added to a level.
-
stop: Callback when an entity is removed from a level (or game switches level).
-
update: Callback for an entity to update its position or state. Often this will use Imu* or GameInput* for a player entity.
-
render: Callback for an entity to render on the canvas. NOTE: This function will typically still be called one additional time after the stop function is invoked!
-
collision: Callback when this entity collider intersects another entity collider. Collision often adjusts the position and may remove one of the objects from the level.
-
event: Callback when a custom event matches this entity description.
The Air Arkanoid game uses the following in their levels/level_game.c file:
static const EntityDescription ball_desc = {
.start = ball_start,
.stop = NULL,
.update = ball_update,
.render = ball_render,
.collision = NULL,
.event = NULL,
.context_size = sizeof(Ball),
};In the above example (ball_desc), the ball_start adds a collider to the ball entity. The update changes the position of the ball entity, sending a custom event to the paddle entity if the ball is off-screen. The render draws the ball on the canvas. The collision callback is not used on the ball entity (when it collides with the paddle or block entity, those callbacks handle the interaction.)
The block_desc below uses the callbacks slightly differently. When the block_desc is created, it's position is set, and a rectangular collider is added to the entity; so the start callback is NULL. The block doesn't move, so the update is also NULL. The render draws the block on the canvas. The collision callback checks if the other object is a ball, and if so, it adjusts the ball speed vector, removes the block entity from the level, plays a sound, and if there are 0 blocks remaining it switches the level to a "You win!" message level.
static const EntityDescription block_desc = {
.start = NULL,
.stop = NULL,
.update = NULL,
.render = block_render,
.collision = block_collision,
.event = NULL,
.context_size = sizeof(Block),
};