Optimized Customizable Entities
Tutorial created by WinterNox.
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:
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:
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 for a level (500 KB).
Limited Fix
A simple fix can be to declare our callback function beforehand as:
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. 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.luabackground_graphics.luabaf_graphics.luamanifest.json
If you need a basic manifest.json, here is a template you can use:
{
"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:
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:
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:
-- 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
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:
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:
- Creating a customizable entity at (
x,y) and storing its ID asentity_id. - Setting the appropriate mesh for the entity.
- Rotating the entity's mesh by a random angle between 0 and 360 degrees about the x-axis.
- Enabling position and angle interpolation as our entity is going to be moving and rotating. (Angle interpolation is enabled by default.)
- Setting an appropriate collision radius and visibility radius for the entity.
The resulting code should look something like this:
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):
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.
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:
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:
- Accessing the entity's data by indexing
bafs_entity_datawith the keyentity_id. - Reversing the entity's direction by multiplying its x-velocity by
-1. If the entity collides with a wall while having an x-velocity of5fx, it must be moving to the right and multiplying the velocity by-1makes the new x-velocity-5fxand the entity begins moving to the left and vice versa.
The resulting code should look something like this:
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:
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, 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:
- Accessing the entity's data.
- Damaging the entity by decrementing its health (has an index
[2]in our table) by1. - Making the entity explode if its health is less than or equal to
0. - 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:
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:
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:
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:
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:
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:
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:
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:
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:
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() (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.