From 6821a588851123556def6189f44ea17d2eadf2e3 Mon Sep 17 00:00:00 2001 From: WinterNox <121163909+WinterNox@users.noreply.github.com> Date: Thu, 21 May 2026 17:13:35 +0530 Subject: [PATCH] Add tutorial for optimized customizable entities --- .../Other/Optimized-customizable-entities.mdx | 402 ++++++++++++++++++ 1 file changed, 402 insertions(+) create mode 100644 docs/Guides/Other/Optimized-customizable-entities.mdx diff --git a/docs/Guides/Other/Optimized-customizable-entities.mdx b/docs/Guides/Other/Optimized-customizable-entities.mdx new file mode 100644 index 0000000..ddfedaf --- /dev/null +++ b/docs/Guides/Other/Optimized-customizable-entities.mdx @@ -0,0 +1,402 @@ +--- +sidebar_position: 3 +--- + +# Optimized Customizable Entities + +## Customizable Entities + +Customizable entities in PewPew Live are a special type of entities that can be customized by the level creator in various ways such as setting a custom mesh, setting an update callback, setting callbacks for interactions between the entity and other parts of the level, configuring its response to game music, etc. The function in the PPL Lua API to create a customizable entity is: + +```lua +pewpew.new_customizable_entity( + x : FixedPoint, + y : FixedPoint +) : EntityId +``` + +## Naive Implementation + +The ways in which customizable entities are handled in a level vary a lot. A common implementation can be of the following form: + +```lua +function my_entity(x, y) + local id = pewpew.new_customizable_entity(x, y) + + pewpew.customizable_entity_set_mesh(id, "/dynamic/graphics.lua", 0) + + pewpew.entity_set_update_callback(id, function() + local ex, ey = pewpew.entity_get_position(id) + + ex = ex + 1fx + ey = ey - 1fx + + pewpew.entity_set_position(id, ex, ey) + end) + + return id +end +``` + +This implementation is fine for an entity that is going to be used infrequently in the level but not ideal for frequent entities. The reason lies in how this code is interpreted. As it is currently, Lua creates an entirely new function every time that we spawn one customizable entity. This is extremely inefficient in terms of memory usage, as every one of those functions takes up some memory. The issue gets worse with larger update callback functions. This can make it extremely easy to hit the [memory limit](https://pewpewlive.github.io/ppl-docs/Other/game-limits#memory-usage) for a level (500 KB). + +## Limited Fix + +A simple fix can be to declare our callback function beforehand as: + +```lua +local function my_entity_update_callback(entity_id) + local ex, ey = pewpew.entity_get_position(entity_id) + + ex = ex + 1fx + ey = ey - 1fx + + pewpew.entity_set_position(entity_id, ex, ey) +end + +function my_entity(x, y) + local id = pewpew.new_customizable_entity(x, y) + + pewpew.customizable_entity_set_mesh(id, "/dynamic/graphics.lua", 0) + + pewpew.entity_set_update_callback(id, my_entity_update_callback) + + return id +end +``` + +This is more efficient in terms of memory usage, but it comes with a major limitation: the inability to access any custom variable that we may use for some purpose, such as `health` or `time`. This significantly limits the complexity of our customizable entity. + +To overcome these problems, we make use of Lua [tables](https://www.lua.org/manual/5.3/manual.html#6.6). Let us see how. + +## Setting Up + +### Goal Entity + +In this example, we will be creating a simple entity similar to the BAF entity in the game. The entity should move horizontally and switch direction when it collides with a wall. It should rotate about its direction of motion (in this case, the x-axis). It should have some `health`. Getting hit by a player bullet should reduce its `health` and it should begin exploding when it runs out of `health`. It should instantly begin exploding upon colliding with the player ship. + +### Files + +Before we can start making the optimized customizable entity, we need to have a base level that we can work with. Start by creating a new folder in `./levels/` (this is next to the ppl-utils executable). In the newly created folder, make sure you have these files: + +- `level.lua` +- `background_graphics.lua` +- `baf_graphics.lua` +- `manifest.json` + +If you need a basic `manifest.json`, here is a template you can use: + +```json +{ + "name":"Sample: Optimized entities", + "descriptions":["An optimized way of creating entities."], + "entry_point":"level.lua" +} +``` + +## Getting Started + +Now that we have a base level, we can start creating a customizable entity and optimizing its memory usage. + +### Setting Up the Level + +Let us quickly set up a basic level with a background that helps us identify the level borders. + +Open `background_graphics.lua` and write the following: + +```lua +meshes = { + { + vertexes = {{0, 0}, {1000, 0}, {1000, 1000}, {0, 1000}}, + colors = {0x0000ffff, 0x0000ffff, 0x0000ffff, 0x0000ffff}, + segments = {{0, 1, 2, 3, 0}} + } +} +``` + +This creates a blue square that is aligned with the level borders. (Our level is going to be `1000fx` by `1000fx`). + +Open `baf_graphics.lua` and write the following: + +```lua +meshes = { + { + vertexes = {{-20, -20}, {20, -20}, {20, 20}, {-20, 20}}, + colors = {0xffff00ff, 0xffff00ff, 0xffff00ff, 0xffff00ff}, + segments = {{0, 1, 2, 3, 0}} + } +} +``` + +This is a simple yellow square. This will be the mesh for our customizable entity. + +Open `level.lua` and start by writing the following: + +```lua +-- Set how large the level will be +pewpew.set_level_size(1000fx, 1000fx) + +-- Create an entity at (0fx, 0fx) that will hold the background mesh +local background_id = pewpew.new_customizable_entity(0fx, 0fx) +pewpew.customizable_entity_set_mesh(background_id, "/dynamic/background_graphics.lua", 0) + +-- Create and configure the player's ship +local player_index = 0 -- There is only one player +local ship_id = pewpew.new_player_ship(250fx, 100fx, player_index) + +local weapon_config = { + frequency = pewpew.CannonFrequency.FREQ_10, + cannon = pewpew.CannonType.DOUBLE +} +pewpew.configure_player_ship_weapon(ship_id, weapon_config) +``` + +## Entity + +### Table for Storing + +:::note + +It is usually recommended to have the code for your customizable entities in separate files for code maintainability. However, we shall have the code in `level.lua` itself in this tutorial for convenience. + +::: + +We can now start working on our customizable entity. Let us see how Lua tables are used to efficiently access custom data about an entity. Designate an area in `level.lua` for the code of our entity and write the following: + +```lua +local bafs_entity_data = {} +``` + +This will be the table in which we will store the custom data of our entity. It is a key-value pair table. We will later use entity IDs as keys. + +### Spawn Function + +Let us create the function for spawning our customizable entity. Create a function `new_baf(x, y)` below the declaration of the `bafs_entity_data` table. + +Start by: + +1. Creating a customizable entity at (`x`, `y`) and storing its ID as `entity_id`. +2. Setting the appropriate mesh for the entity. +3. Rotating the entity's mesh by a random angle between 0 and 360 degrees about the x-axis. +4. Enabling position and angle interpolation as our entity is going to be moving and rotating. (Angle interpolation is enabled by default.) +5. Setting an appropriate collision radius and visibility radius for the entity. + +The resulting code should look something like this: + +```lua +function new_baf(x, y) + local entity_id = pewpew.new_customizable_entity(x, y) + + pewpew.customizable_entity_set_mesh(entity_id, "/dynamic/baf_graphics.lua", 0) + pewpew.customizable_entity_set_mesh_angle(entity_id, fmath.random_fixedpoint(0fx, fmath.tau()), 1fx, 0fx, 0fx) + pewpew.customizable_entity_set_position_interpolation(entity_id, true) + pewpew.customizable_entity_set_angle_interpolation(entity_id, true) + pewpew.entity_set_radius(entity_id, 20fx) + + -- Ensure that the entity will not be rendered when not visible + pewpew.customizable_entity_set_visibility_radius(entity_id, 20fx) + + return entity_id +end +``` + +Spawn one of our entities using `new_baf(100fx, 100fx)`. If you save the files and run your level, you should see a yellow square centered at (`100fx`, `100fx`). + +Now, we can use the `bafs_entity_data` table to store information about the entity. Let us see how. For our entity, two pieces of **custom** information are important: + +- Its horizontal velocity +- Its health + +In our `new_baf` function, write the following code (must be after the entity is spawned and its ID is stored as `entity_id`): + +```lua +bafs_entity_data[entity_id] = { + 5fx, -- x-velocity + 3 -- Health +} +``` + +This code adds a new entry to `bafs_entity_data`, with the `entity_id` as key and the table `{5fx, 3}` as value, letting us retrieve information about the entity elsewhere in the code. Let us see how we can use this functionality to code the behavior of the entity. + +### Update Callback + +Let us create an update callback function for our entity. Below the declaration of the table `bafs_entity_data` and above the declaration of the function `new_baf` (since the update callback function needs to be declared before we assign it to the entity), make a new function `baf_update_callback(entity_id)`. This function, when assigned as our entity's update callback, will be called each tick with the entity's ID, which we obtain through our parameter `entity_id`. + +```lua +function baf_update_callback(entity_id) + local entity_data = bafs_entity_data[entity_id] + + local ex, ey = pewpew.entity_get_position(entity_id) + + ex = ex + entity_data[1] + + pewpew.entity_set_position(entity_id, ex, ey) + + pewpew.customizable_entity_add_rotation_to_mesh(entity_id, 0.1000fx, 1fx, 0fx, 0fx) +end +``` + +Since we have the entity's ID, we can access the information about the entity using `entity_data = bafs_entity_data[entity_id]`. In the table `entity_data`, index `[1]` gives the entity's velocity along the x-axis, and index `[2]` gives the entity's health. We therefore access the x-velocity using `entity_data[1]` and add it to the entity's x-position. We also slightly rotate the entity's mesh along the x-axis (by `0.1000fx` 30 times per second). + +We need to assign this callback function to the entity. In the `new_baf` function code, after we store the entity's information into `bafs_entity_data`, write the following code: + +```lua +pewpew.entity_set_update_callback(entity_id, baf_update_callback) +``` + +If you save the files and run your level, you should see the yellow square moving to the right. + +### Wall Collision Callback + +Currently, our entity will keep moving in the same direction indefinitely. To fix that, let us create a wall collision callback for our entity. We want it to switch directions when it collides with a wall. Below the declaration of the table `bafs_entity_data` and above the declaration of the function `new_baf`, make a new function `baf_entity_wall_collision_callback(entity_id, wall_normal_x, wall_normal_y)`. This function, when set as our entity's wall collision callback, is called every time that our entity collides with a wall, with the entity's ID and information regarding the normal to the wall. We obtain the entity ID and normal information through our function's parameters. In this tutorial, we do not make use of the normal information. + +Start by: + +1. Accessing the entity's data by indexing `bafs_entity_data` with the key `entity_id`. +2. Reversing the entity's direction by multiplying its x-velocity by `-1`. If the entity collides with a wall while having an x-velocity of `5fx`, it must be moving to the right and multiplying the velocity by `-1` makes the new x-velocity `-5fx` and the entity begins moving to the left and vice versa. + +The resulting code should look something like this: + +```lua +function baf_entity_wall_collision_callback(entity_id, wall_normal_x, wall_normal_y) + local entity_data = bafs_entity_data[entity_id] + + entity_data[1] = -entity_data[1] +end +``` + +We need to assign the callback function. Right after we set the update callback, write the following code: + +```lua +pewpew.customizable_entity_configure_wall_collision(entity_id, true, baf_entity_wall_collision_callback) +``` + +The second argument `true` tells the API that we want our entity to collide with walls, and the third argument sets the callback. If you save the files and run your level, you should see the entity reversing its direction upon colliding with a wall. **This behavior of our entity is only possible because we were able to use a custom variable (the x-velocity) using tables.** + +### Weapon Collision Callback + +Let us quickly code the remaining behavior for our entity. Make another function `baf_entity_weapon_collision_callback(entity_id, player_index, weapon_type, x, y)`. This function, when set as our entity's weapon collision callback, is called every time our entity collides with a weapon, with the entity's ID, the index of the player which triggered the weapon, the [weapon type](https://pewpewlive.github.io/ppl-docs/APIs/PewPew#weapontype), and the velocity vector of player bullets or origin of explosions. We obtain the information through our function's parameters. In this tutorial, we do not make use of the player index, the weapon type, or information about the velocity vector or origin of explosion. If you have multiple weapon types in your level, wish to increment the player's score, or have your entity be pushed backwards by player bullets and explosions, you would use these parameters. + +Start by: + +1. Accessing the entity's data. +2. Damaging the entity by decrementing its health (has an index `[2]` in our table) by `1`. +3. Making the entity explode if its health is less than or equal to `0`. +4. Returning `true`. This tells the game whether our entity reacts with the weapon it collided with. In our case, it results in the destruction of the player bullet which it collided with. + +The resulting code should look something like this: + +```lua +function baf_entity_weapon_collision_callback(entity_id, player_index, weapon_type, x, y) + local entity_data = bafs_entity_data[entity_id] + + entity_data[2] = entity_data[2] - 1 + + if entity_data[2] <= 0 then + pewpew.customizable_entity_start_exploding(entity_id, 10) + end + + return true +end +``` + +Assign it to our entity. Right after we set the wall collision callback, write the following code: + +```lua +pewpew.customizable_entity_set_weapon_collision_callback(entity_id, baf_entity_weapon_collision_callback) +``` + +If you save the files and run your level, you should now be able to damage the entity and kill it. + +### Player Ship Collision Callback + +Write the following code: + +```lua +function baf_entity_player_collision_callback(entity_id, player_index, ship_entity_id) + pewpew.customizable_entity_start_exploding(entity_id, 20) +end +``` + +The callback is called with the entity ID, the player index of the player that the entity collided with, and the player's ship's entity ID. In this tutorial, we will not make use of the player index and ship entity's ID. If you want to increment the player's score or damage the ship, you would use these parameters. Right after we set the weapon collision callback, write the following code: + +```lua +pewpew.customizable_entity_set_player_collision_callback(entity_id, baf_entity_player_collision_callback) +``` + +If you save the files and run your level, the entity should now instantly begin exploding upon colliding with the player ship. Our entity is almost finished! + +### Freeing the Memory + +Right now, when the entity dies, it simply stops accessing its data in the table `bafs_entity_data`. But, the data is still there, consuming memory. This is an issue as it means that the memory being used keeps increasing in the level as we spawn more entities. We need to cleanly delete the data once the entity starts exploding. To do this, we write the following code inside our `baf_entity_update_callback` function at the very top: + +```lua +if pewpew.entity_get_is_started_to_be_destroyed(entity_id) then + bafs_entity_data[entity_id] = nil +end +``` + +This removes the value associated with the key `entity_id`. However, we continue to try to access the data when the entity is being destroyed. This leads to errors as we are trying to index `entity_data` which is `nil`. To stop this, we remove all the callbacks once the entity starts being destroyed. This is done by setting `nil` as our callbacks. Update the above code to: + +```lua +if pewpew.entity_get_is_started_to_be_destroyed(entity_id) then + bafs_entity_data[entity_id] = nil + + pewpew.entity_set_update_callback(entity_id, nil) + pewpew.customizable_entity_configure_wall_collision(entity_id, true, nil) + pewpew.customizable_entity_set_weapon_collision_callback(entity_id, nil) + pewpew.customizable_entity_set_player_collision_callback(entity_id, nil) + + return +end +``` + +Now we are cleanly deleting the entity's data once it starts getting destroyed. We use `return` to exit out of the `baf_entity_update_callback` function immediately after deleting the data. This ensures that the lines that follow, which access the data, are not executed. We can now spawn a lot of our entities without having to worry about hitting the memory limit. Spawn **1200** of our entities using the following for-loop: + +```lua +for i = 1, 1200 do + local x = fmath.random_fixedpoint(100fx, 900fx) + local y = fmath.random_fixedpoint(100fx, 900fx) + + new_baf(x, y) +end +``` + +Save the files. + +## Alternative Table Structure + +Currently, the table containing the information for an entity looks like this: + +```lua +bafs_entity_data[entity_id] = { + 5fx, -- x-velocity + 3 -- Health +} +``` + +We are accessing this table as `entity_data` in our callbacks and accessing the values inside the table as `entity_data[1]` and `entity_data[2]`. For more complex entities, our code can be quite difficult to read. Furthermore, if we were to add a new entry into the middle of the table, the numeric indices of all subsequent entries shift. This would force us to manually update the references to those entries in our callbacks. Alternatively, we could store the information like this: + +```lua +bafs_entity_data[entity_id] = { + x_vel = 5fx, + hp = 3 +} +``` + +This lets us access the values inside using dot notation as `entity_data.x_vel` and `entity_data.hp` without having to worry about index shifting. However, this increase in readability and maintainability comes at the cost of slightly higher memory usage. + +## Memory Usage Comparison + +Upon spawning 1200 of our entities, here is what the memory usage looks like, as returned by [`pewpew.print_debug_info()`](https://pewpewlive.github.io/ppl-docs/APIs/PewPew#print_debug_info) (there can be slight differences in memory usage due to variable names): + +| Method | Memory Used (Bytes) | Implementation Details | +| ------ | ------ | --- | +| Unoptimized A | 322,618 | The callback functions are declared inside the spawn function. | +| Unoptimized B | 293,920 | The callback functions **except the player collision callback function** are declared inside the spawn function. The player collision callback of our entity does not require any custom information and hence the function can be declared beforehand. | +| Optimized (Alternative) | 272,385 | The callback functions are declared beforehand and a table of the form `{x_vel = 5fx, hp = 3}` is used to store custom information about entities. | +| Optimized | 224,084 | The callback functions are declared beforehand and a table of the form `{5fx, 3}` is used to store custom information about entities. | + +By using the optimized approach, we save approximately 98,534 bytes (30.54% decrease) of memory compared to Unoptimized A and approximately 69,836 bytes (23.76% decrease) of memory compared to Unoptimized B. This relative difference increases with increase in the size and complexity of the callback functions. + +Congratulations! You just learned how to use Lua tables to optimize the memory usage of your entities. I hope that you were able to follow the tutorial fairly well. Now, experiment with your code and make creative customizable entities! Note that a more complex customizable entity is not always a better one. The files for the complete entity can be found in the [ppl-utils GitHub repository](https://github.com/pewpewlive/ppl-utils/tree/master/levels/sample_optimized_entities).