Stratagem Devlog, Part 2

It’s not easy being tweened

thoughts
gamedev
Author

Vincent “VM” Mercator

Published

October 2, 2024

Modified

October 27, 2024

Since releasing Stratagem to the public in early September, I’ve been organizing and improving its code, implementing new features, and soliciting feedback from other game developers. It’s been nice seeing what people think of my game; having other people look at my project and take interest in it helps me find issues that would have taken me much more time to find on my own.

Note

This article is part of a series called “Stratagem Devlog”.

Artwork, Animation, and Particles

The first and most important thing I would like to highlight is the new artwork and animations added to the game. The game’s first external contributor, MattSquare (@squaremango), redesigned the basic sprites that I made in 2019 to better resemble actual gem cuts.

A comparison between two sets of gem sprites. The bottom set by MattSquare looks more detailed.

My sprite artwork was OK, but it made the gems look more like beveled shapes. MattSquare’s gem sprite artwork adds more depth to the gems.

I’ve complemented the new gem sprites from MattSquare with animations and particle effects to give the game more polish. Making room for most of these visual effects was easy: all they needed were extra intermediate states in the game’s internal finite-state machine. I plan on moving more common animation-related operations like linear interpolation and easing into their own functions to further neaten up the code.

The one animation that needed the most amount of work, though, was the gem-falling animations. This one runs dynamically back-to-back after all matches are removed from the grid. The animation’s cycle keeps getting interrupted by the logic for adding extra gems to the grid. It took some tweaking with state transitions and messing with frame counters to get it to look the way I wanted without making the result look choppy.

Multicolored gems drop downward on a six-by-six grid. The animations are more fluid, and the sprite artwork looks more detailed.

Animated gameplay of Stratagem v0.3.1. The animations help give the game more polish.

Some members of the Bejeweled Fans Discord server suggested the sparkly gem-matching animations, which are represented with particles. A single particle is a table of five values: the coordinate of the gem it’s centered around, its position (in polar coordinates) away from that gem, its radial velocity, and its radial acceleration. I chose to use both the game grid’s coordinates and polar coordinates so that I only needed to keep track of the particle’s acceleration and velocity pointing away from the gem it’s centered around.

-- [[ in src/types.lua ]]

---@class Particle # a particle used for match animations
---@field coord Coords # center gem coordinate, used to determine the particle's relative origin
---@field r number # radius [px] from the particle's relative origin
---@field theta number # angle [rotations] from the particle's relative origin
---@field vr number # radial velocity [px/s] from the particle's relative origin
---@field ar number # radial acceleration [px/s^2] from the particle's relative origin

-- [[ in src/ui.lua ]]

---@type Particle[] list of particles for matches
Particles = {}

-- [[ example particle ]]

local particle = { coord = { x = 2, y = 3 }, r = 0, theta = 0.125, vr = 40, ar = -60 }
-- the pixel coordinates of this particle's relative origin is { x = 40, y = 56 } [px]

P8 Lua has its own separate standard library, which is different from the one used in vanilla Lua 5.2. One important quirk that I had to watch out for is how the cos() and sin() functions use rotations as angle units instead of degrees or radians, and that the sin() function is inverted from its definition in mathematics. If I had to guess, these design choices were probably made to make them more understandable to children and that circular animations using multiples of these functions would rotate clockwise instead of counterclockwise.

The particle’s acceleration value stays constant. Its velocity and position values are naïvely recalculated on every frame using the forward Euler method: multiply the derivative by the time step.1 Here is the relevant code in the DrawMatchAnimations() function that shows off how the particle’s properties change over time.

-- [[ in src/ui.lua ]]

---@param player Player # Player object
---@param frame integer # frame index, in the range [0, MATCH_FRAMES]
-- Draw the point numbers for the player's match where the gems were cleared
function DrawMatchAnimations(player, frame)
    -- initialize particles
    if frame == 0 then
        Particles = {}
        for _, coord in ipairs(player.last_match.match_list) do
            for i = 1, 8 do
                add(Particles, { coord = coord, r = 0, theta = 0.125 * i + rnd(0.125), vr = 40, ar = -60 })
            end
        end
    end
    local particle_progress = frame / MATCH_FRAMES
    -- ...
    if player.combo ~= 0 then
        -- ...
        for _, particle in ipairs(Particles) do
            local particle_origin = { x = 16 * particle.coord.x + 8, y = 16 * particle.coord.y + 8 }
            circfill(
                particle_origin.x + particle.r * cos(particle.theta),
                particle_origin.y + particle.r * sin(particle.theta),
                3 - 3 * particle_progress,
                GEM_COLORS[player.last_match.gem_type]
            )
            particle.vr = particle.vr + 1 / 30 * particle.ar
            particle.r = particle.r + particle.vr * 1 / 30
        end
        -- ...
    end
end

I’m no animation expert, but I think the way in which the particles move is called a quadratic ease-out, since the particle’s radial displacement trajectory from its relative origin follows a parabolic arc over time. I’m probably going to rewrite this code later by isolating the actual trajectory into its own easing function like what I did with linear interpolation.

Plot of the particle’s displacement, velocity, and acceleration as functions of time.

Swing and Polyrhythms in PICO-8

Each individual sound effect in the PICO-8’s music tracker can contain up to 32 notes each, which makes the PICO-8 tracker best suited for music with time signatures that are multiples of 2 with straight eighth-notes and sixteenth-notes. That won’t stop me from trying to write jazz pieces, though! I’ve added a fourth music piece called “Midnight Oil” that takes advantage of how sound effects that run at different speeds loop in PICO-8 to add swing.

In the PICO-8’s sound effect editor, a sound effect’s SPD value controls the time (in ticks) to wait before playing the next note in the sound effect. Since you can export these sound effects to .wav files sampled at 22.05 kHz, and a sound effect made of a single note at SPD = 1 is 183 samples long, you can figure out that a single tick is 183/22,050 (~0.0083) seconds long.

When a sound effect has a LOOP parameter, it will repeatedly play in a track pattern until all sound effects in that track pattern end. By cleverly manipulating the SPD and LOOP parameters (like in the screenshots below) of two tracks playing simultaneously, you can make polyrhythms, swing, or non-4/4 time signatures. In my case, one track plays 24 notes in the same time it takes for another to play 32, which forms a 4:3 polyrhythm. This lets me play eighth-note triplets alongside quarter notes.

\[ \begin{aligned} \frac{1 \text{ [note]}}{12 \text{ [tick]}} & \times \frac{1 \text{ [beat]}}{8 \text{ [note]}} & \times \frac{1 \text{ [tick]}}{\frac{183}{22,050} \text{ [s]}} & \times \frac{60 \text{ [s]}}{1 \text{ [min]}} & \approx 75.31 \frac{\text{ [beat]}}{\text{ [min]}} \\ \frac{1 \text{ [note]}}{16 \text{ [tick]}} & \times \frac{1 \text{ [beat]}}{6 \text{ [note]}} & \times \frac{1 \text{ [tick]}}{\frac{183}{22,050} \text{ [s]}} & \times \frac{60 \text{ [s]}}{1 \text{ [min]}} & \approx 75.31 \frac{\text{ [beat]}}{\text{ [min]}} \end{aligned} \]

The notes in this drum track play slower, but each beat is made of 6 notes.

The notes in this bass track play faster, but each beat is made of 8 notes.

Here’s the corresponding sheet music for what the two sound effects from the screenshots will sound like when played together.

This is a more literal sheet music translation, using a 12/8 time signature instead of 4/4. It forces the swing ratio to be 2:1 by changing what gets defined as a beat. Please don’t write jazz music like this.

Next Steps

The most interesting feature requests from players are the ones that I hadn’t thought of already. One of these requests was to allow the player to move the grid cursor while gems were matching and falling in the grid. In older versions, the player would have to wait for all the gems to settle in the grid before moving their cursor to the next match, which stole away controls from the player for letting them plan their next move. I didn’t expect how this change be such a quality-of-life improvement by speeding up gameplay.

The most popular feature requested (by two people) has been adding mouse controls. This was something I hadn’t considered, since the PICO-8’s mouse controls are still considered experimental. As of writing this article, I’m still trying to get mouse controls to work nicely. It should be ready by the next release, and I’ll go into more detail about how it works in the development log. Stay tuned!

Footnotes

  1. Astute viewers of my site would recognize the forward Euler method from prompt 8 of Genuary 2024.↩︎

Reuse

CC BY SA 4.0 International License(View License)