Skip to main content

60 FPS Animation

Tutorial created by WinterNox.

YouTube tutorial

Mesh animations

Mesh animations in PewPew Live are achieved by iterating through multiple meshes that are slightly different from each other. The meshes are usually stored in a single file and are made procedurally.

Limitations

If you have some experience with level creation, you should know that PewPew Live runs at 30 ticks per second and that the graphics are interpolated. However, the interpolation only applies to transformations (position, rotation and scale). But, what about mesh animations? Since you can only change something about the entity per tick, mesh animations would be limited to 30 frames per second. Thankfully, the PPL Lua API has just the right function that can be used to achieve 60 FPS mesh animations. This function is:

pewpew.customizable_entity_set_flipping_meshes(entity_id, file_path, index1, index2)

When used, the entity will switch between the two specified meshes in a single game tick. But, how does that help us achieve 60 FPS animations? Let's see.

Setting up

Before we can start making an animation and assigning it to an entity, we need to have a base level that we can work with. Start by creating a new folder in ./content/levels/. In the newly created folder, make sure you have these files:

  • level.lua
  • graphics.lua (The name of your mesh file can be different, but remember it!)
  • manifest.json

If you need a basic manifest.json, here is a template you can use.

{
"name": "60 FPS",
"descriptions": ["Amazing"],
"entry_point": "level.lua",
"has_score_leaderboard": false
}

Getting started

Now that we have a level base, we can start making our mesh and entity. If at any point you face a problem, try following the steps again, or check the final .lua files.

Making the mesh

Open graphics.lua (or your mesh file) and start by writing the following:

meshes = {}

Since our meshes are going to be procedurally generated, we won't be making every vertex by hand, instead, we will make Lua do that for us! Essentially when making a procedural mesh, we make a table with our vertexes and segments, that we later add to the meshes table. The vertex and segment tables are going to hold the generated points and segment indexes.

Add this to your mesh file:

-- Tables for our mesh vertexes, segments
local computed_vertexes, computed_segments = {}, {}

Now we need a way to populate the tables with our mesh points and segments.

Note: In this example, we will be making a hexagon!

Start by defining the radius and small_radius variables. They will be used when generating the hexagon sides. Also, create a variable i, which will be used for the segments. Now, create a for loop that will go from 0 to math.pi * 2 and increment by math.pi * 2 / 6 (since we want it to be a hexagon). Get the corresponding sine and cosine of the angle (y and x positions). Multiply one pair by the default radius and one by the smaller radius, and add them to the vertexes table. To add the needed segment indexes, we need to connect the vertexes together. Connect the inner hexagon vertex to the outer one, connect the inner hexagon vertex with its own next vertex, and connect the outer hexagon vertex the same way. Increment the segment counter by 2.

The resulting code should look something like this:

local radius = 96  -- Outer hexagon
local small_radius = 48 -- Inner hexagon

local i = 0
for angle = 0, math.pi * 2, math.pi * 2 / 6 do -- We want to go from angle 0 to 2π, skipping by 2π / 6, making a hexagon
local y, x = math.sincos(angle) -- y and x positions on the unit circle

table.insert(computed_vertexes, {x * radius, y * radius}) -- Vertex for the outer hexagon
table.insert(computed_vertexes, {x * small_radius, y * small_radius}) -- Vertex for the inner hexagon

table.insert(computed_segments, {i, i + 1}) -- Line joining the corresponding vertices of the inner and outer hexagons
table.insert(computed_segments, {i, i + 2}) -- Line joining the vertex of the outer hexagon to its next one
table.insert(computed_segments, {i + 1, i + 3}) -- Line joining the vertex of the inner hexagon to its next one

i = i + 2
end

Because of the way we added the segments, at the end of the for loop we get extra segments that are unnecessary. After the for loop, we can remove them by doing this:

table.remove(computed_segments, #computed_segments)  -- Removal of the last segment as there is no vertex at index i + 3 during the last iteration
table.remove(computed_segments, #computed_segments) -- No vertex at index i + 2 during the last iteration
table.remove(computed_segments, #computed_segments) -- A line is already present joining the vertices

At the end, we add the mesh made up of the generated vertexes and segments.

table.insert(meshes, {
vertexes = computed_vertexes,
segments = computed_segments
})

Creating the entity and assigning it the mesh

Now, go to level.lua, and start by creating an entity at position (0fx, 0fx). Next, set the entity's mesh to the one we just created.

local id = pewpew.new_customizable_entity(0fx, 0fx)
pewpew.customizable_entity_set_mesh(id, "/dynamic/graphics.lua", 0) -- Change the filename if needed

Note: Setting level size and creating a player is optional.

Run ppl-utils.exe and open your level at http://localhost:9000/pewpew.html

You should see the following: Initial mesh

If you don't, you might have made a mistake somewhere! Check the steps to make the mesh again and check for mistakes.

Animating the mesh

Let's get into animating our mesh! For this tutorial, we will have the outer hexagon rotating clockwise and the inner hexagon counterclockwise.

To do this, we need multiple "meshes" that we will set our entity to.

In your mesh file, change for loop to the following:

for angle_offset = 0, math.pi * 2, math.pi * 2 / 60 do  -- Our animation will have 60 frames. We want the hexagons to make a full rotation each 60 frames (2 seconds). This does not mean the animation is 60FPS yet!
-- Tables for our mesh vertexes, segments
local computed_vertexes, computed_segments = {}, {}

local radius = 96 -- Outer hexagon
local small_radius = 48 -- Inner hexagon

local i = 0
for angle = 0, math.pi * 2, math.pi * 2 / 6 do -- We want to go from angle 0 to 2π, skipping by 2π / 6, making a hexagon
local y, x = math.sincos(angle) -- y and x positions on the unit circle

table.insert(computed_vertexes, {x * radius, y * radius}) -- Vertex for the outer hexagon
table.insert(computed_vertexes, {x * small_radius, y * small_radius}) -- Vertex for the inner hexagon

table.insert(computed_segments, {i, i + 1}) -- Line joining the corresponding vertices of the inner and outer hexagons
table.insert(computed_segments, {i, i + 2}) -- Line joining the vertex of the outer hexagon to its next one
table.insert(computed_segments, {i + 1, i + 3}) -- Line joining the vertex of the inner hexagon to its next one

i = i + 2
end

table.remove(computed_segments, #computed_segments) -- Removal of the last segment as there is no vertex at index i + 3 during the last iteration
table.remove(computed_segments, #computed_segments) -- No vertex at index i + 2 during the last iteration
table.remove(computed_segments, #computed_segments) -- A line is already present joining the vertices

table.insert(meshes, {
vertexes = computed_vertexes,
segments = computed_segments
})
end

As you can see, the real mesh generation code is pretty much the same, just that we repeat it several times instead, making it have frames!

Now, most importantly, we need to change the way we get the points of our hexagon. Since we want the outer hexagon to turn clockwise and the inner one counterclockwise, we need to change the angles depending on which frame we are on. Since each frame is going to have a specific angle offset, we can add that to our initial angles, meaning we have to change this line:

local y, x = math.sincos(angle)

into

local y, x = math.sincos(angle - angle_offset)

And add a new line below it like so:

local y2, x2 = math.sincos(angle + angle_offset)

We will use x2 and y2 for the inner hexagon. To do this, replace the x * small_radius and y * small_radius coordinates for the inner hexagon with x2 * small_radius and y2 * small_radius respectively.

We now have 60 frames of meshes that we can use to animate our entity. To do this, we need to use an index which will increase every tick, and use that to set the entity's mesh. This can be done like so: In level.lua, replace the second line with this code:

local mesh_index = 0
pewpew.entity_set_update_callback(id, function()
-- We have 60 frames out of 61 total frames. The last frame is equal to the first one and is unused.
-- The index of the 60th frame is 59. (Although we are using lua, mesh and sound indexes start from 0 in PewPew Lib API.)
-- Loop when we have exceeded past the last frame.
if mesh_index > 59 then
mesh_index = 0
end

pewpew.customizable_entity_set_mesh(id, "/dynamic/graphics.lua", mesh_index)
mesh_index = mesh_index + 1
end)

The code is pretty self-explanatory. We increment the index every tick and use that index to set the mesh. Since the meshes we have generated have small changes in them, doing this creates an animation.

The result should look something like this:

As you can see, it is an animation. Though, this animation is running at only 30 frames per second. This is an issue. An animation running at 30 FPS will not look good with the rest of the level as the level might have other content that moves. And as you already know, position transformation is interpolated by PewPew Live. This means that, even though the level runs at 30 ticks per second, the game will interpolate the graphics and display them at 60 FPS or 90 FPS for example. (The game interpolates the graphics as per the refresh rate of the device, which will usually be higher than 30Hz.) To ensure that the difference between the rest of the level and our mesh animation is minimal, we have to make our mesh animation run on at least 60 FPS.

Making the animation 60 FPS

Open your mesh file and replace the line

for angle_offset = 0, math.pi * 2, math.pi * 2 / 60 do  -- 60
-- Code that generates the meshes
end

with

for angle_offset = 0, math.pi * 2, math.pi * 2 / 120 do  -- 120
-- Code that generates the meshes
end

The new code will create 121 frames or meshes for us, out of which 120 will be used. (This is because the last and first frames are the same.)

If you save the files and run your level, you might notice that the animation is getting reset halfway through. This is because at the 60th frame, we have gone only 50% through the animation.

Since we now have 120 frames, we need to change the code in level.lua to account for this change. Open level.lua and change the line

if mesh_index > 59 then
mesh_index = 0
end

to

if mesh_index > 119 then
mesh_index = 0
end

The new code now shows the complete animation when running the level. However, it is visibly slower. Why is this the case? Remember, in the previous case, the variable angle_offset was increasing by 2π/60 every frame. Now, it is increasing by 2π/120 every frame. You might be wondering why we did this. This is because to achieve a 60 FPS animation that runs with the same speed, we need to have double the amount of frames that we had previously. We now just need to speed the animation back to its original speed.

Open level.lua and change

mesh_index = mesh_index + 1

to

mesh_index = mesh_index + 2

This should make the animation return to its original speed, as we are skipping every other frame. However, if you save the files and run your level, you will notice no change between the current animation and the one we had at the start. You would in fact be right, there is no difference between the two. Our animation still runs at 30 FPS. This is where flipping meshes comes into play.

Open level.lua and replace

pewpew.customizable_entity_set_mesh(id, "/dynamic/graphics.lua", mesh_index)

with

pewpew.customizable_entity_set_flipping_meshes(id, "/dynamic/graphics.lua", mesh_index, mesh_index + 1)

What does this exactly do? When we set flipping meshes of an entity, that entity goes over the two specified meshes in a single game tick. This means that, in the first game tick, it will have the mesh at index 0 for half a tick and will have the mesh at index 1 for the rest half of a tick. In the next tick, we are resetting the flipping meshes. The variable mesh_index now has a value of 2. The entity now has the mesh at index 2 for half a tick and has the mesh at index 3 for half a tick. Notice that we have gone over 4 meshes in just 2 game ticks (2 meshes per game tick)! The way in which we have used this creates an animation that runs at 60 FPS. Now, if you save the files and run your level, you'll notice that it is a lot smoother than our initial animation. It is exactly twice as smooth. It might look something like this:

Congratulations! You have successfully made an animation that runs at 60 frames per second. I hope that you were able to understand this fairly well. Now, experiment with your code and make creative animations.