Making a pinball game for Playdate: Part 11, the spinner spring

Welcome to this adventure, where I write about the process of our latest game, Devils on the Moon pinball.

We are ramping up to the final stages of the game, so I had to cross off one of our long standing pending features: The Pinball spinner

From the beginning of development, when the idea to make a pinball game started, we were excited about the spinner. Jp had to figure out a way to use Blender to generate rotated sprites that looked good (hopefully one day he will have enough time to talk about it here). And we played the physical table of Pulp Fiction, and we fell in love with its spinner.

Pulp fiction

So Jp started working and soon enough had a good spinner sprite for me to implement in to the game.

spinner

Now we just needed a way to make it spin. The first step I needed was to detect if the ball was colliding with the spinner in some way. This was the first time we needed to know if the ball was inside a collision shape but didn't affect it as a physics body. So I implemented the sensor system.

The Sensors

Each sensor component has two buffers with a list of entity handles. Each frame it queries entities inside its collision shape using the spacial hashing and then compares the new list with the previous list of entities.

If there is an entity that is not on the previous frame list, it sends the event body_entered. If an entity is missing from the previous frame, it sends the event body_exited.

As we only have one ball, at least for the main table, we are probably not going to have multi-ball. The check is really simple; I just go through both arrays and compare them one to one.

Another problem was, how do we decide which sensors should update each frame? We have over 70 sensors in the main table, some of them with complex polygon collision shapes. So I decided to use the ball to tell which one to update. I query a circle at the position of each of the balls that's 4 times the radius of the ball and mark all the overlapping sensors as dirty. Then we go through all the dirty sensors and update them.

Back to the spinner

Having the sensors set up, the spinner logic became easy. If there is a ball inside the spinner sensor, the spinner angular velocity is equal to the ball velocity. And when it exits, I just apply a damping factor so the spinner would eventually stop.

Each 0.5f turn we count it as a spin, so we compared the previous angle to the new one, and if it changed enough, we notified a spin.

b32 did_spin = false; if(spinner->handle.id != 0) { entity *ball = pinball_get(pinball, spinner->handle); spinner->vel = v2_len_sq(ball->body.vel); } f32 vel = spinner->vel; f32 t = spinner->t + (vel * dt); i32 t_a = floor_f32(spinner->t * 2.0); i32 t_b = floor_f32(t * 2.0); if(t_a != t_b) { did_spin = true; spinner->spins++; } spinner->vel = vel * spinner->damp; spinner->t = t; return did_spin;

The only problem with this is that the spinner would stop at some awkward rotation and stay like that until the ball entered again. That's not how pinball spinners work! They have a weight at the tips to make sure it always ends up perpendicular to the table. It worked but didn't feel as good.

spinner-diablo

This is how the spinner in Catchadiablos works, by the way.

Springs

The first time I was reading about using springs for animation was from this great article by Josh W. Comeau. The article is full of interactive examples that helped the concept click. Since then I have used it sparingly for some little animations on our games. Another great resource is this video on a small script for Godot that helps you animate almost anything using springs.

When I started thinking about our spinner problem, I thought I would need to simulate some kind of pendulum using physics and that my cheap trick of just using the ball velocity was going away. And that's why I put if off for a long time, until I got this article on my RSS feed on springs and all their utilities. It's great! I greatly recommend it.

So I started thinking these springs surely look like the motion a pinball spinner.

I didn't have to change that much; just make sure to record the starting direction of the spinner as it will bounce back and forth until it gets to its resting angle.

Calculate the target turn and apply a spring force to get there.

And stop registering spins after the spinner changes direction for the first time.

b32 spinned = false; f32 angle_prev = spinner->angle_turns; if(spinner->entity_handle.id != 0) { entity *ball = pinball_get(pinball, spinner->entity_handle); spinner->direction = sgn_f32(ball->body.vel.y); spinner->angular_vel = v2_len_sq(ball->body.vel); spinner->register_spins = true; } f32 angle = spinner->angle_turns; f32 angular_vel = spinner->angular_vel; spinner->angle_turns = angle + (angular_vel * dt); f32 k = spinner->stiffness; f32 c = spinner->damping; f32 target = round_f32(spinner->angle_turns * 2.0f) * 0.5f; f32 accel = -k * (spinner->angle_turns - target) - c * spinner->angular_vel; spinner->angular_vel = spinner->angular_vel + accel * dt; f32 prev_half = floor_f32(angle_prev * 2.0f); f32 curr_half = floor_f32(spinner->angle_turns * 2.0f); if(curr_half != prev_half && spinner->register_spins) { f32 delta = spinner->angle_turns - angle_prev; if(sgn_f32(delta) == spinner->direction) { spinned = true; spinner->spins++; } else { spinner->register_spins = false; } } return spinned;
spinner spring

And now it looks great! So yeah, springs, I just think they are neat.

Comments

Other Posts

Archive

You can subscribe via RSS or follow us @amanogames_

Making a pinball game for playdate: Part 10, the events and actions

Making a pinball game for playdate: Part 10, the events and actions

Events and actions handle most of the logic of our pinball game

Making a pinball game for Playdate: Part 09, the ball HUD

Making a pinball game for Playdate: Part 09, the ball HUD

How do we show how many chances you have left?

Making a pinball game for Playdate: Part 08, the entities and their components

Making a pinball game for Playdate: Part 08, the entities and their components

How do we organize our game entities and their components.

Making a pinball game for Playdate: Part ??, the secret project

Making a pinball game for Playdate: Part ??, the secret project

What happened in the last six months?

Making a pinball game for Playdate: Part 07, the debugger

Making a pinball game for Playdate: Part 07, the debugger

Searching for a debugger on Linux

Making a pinball game for Playdate: Part 06, the profiler

Making a pinball game for Playdate: Part 06, the profiler

Learning how to use a profiler

Making a pinball game for Playdate: Part 05, the spatial partition

Making a pinball game for Playdate: Part 05, the spatial partition

2 Bits image formats.

Making a pinball game for Playdate: Part 04, the image format

Making a pinball game for Playdate: Part 04, the image format

2 Bits image formats.

Making a pinball game for Playdate: Part 03, the first level editor

Making a pinball game for Playdate: Part 03, the first level editor

How did we choose our first level editor for the game?

Making a pinball game for Playdate: Part 02, the physics

Making a pinball game for Playdate: Part 02, the physics

Let's talk about physics.

Making a pinball game for Playdate: Part 01, the language

Making a pinball game for Playdate: Part 01, the language

Welcome to this December adventure, where I will try to write about the process of our last game, Devils on the Moon pinball. Today I will talk about our choice of programming language for the game.

Let’s finish this

Let’s finish this

We are back working on Pullfrog! What happened?

Let's talk about Don Salmon

Let's talk about Don Salmon

Don salmon, a new platforming game made in Godot and a small update on Pullfrog

Spooky eyes and level editors

Spooky eyes and level editors

Last year we made the decision to take a break and focus on a spooky game around the spooky season.

This kills the frog

This kills the frog

After rewriting the physics system for the third time, it was time to start working on more fun stuff. The frog death system™.

On starting a game

On starting a game

A couple of things I would recommend when starting your first game on the Playdate.

How to correct a corner

How to correct a corner

There are many techniques that you can apply so that a platformer game feels good. One of those is corner correction.

On "Bouncy" Animation

On "Bouncy" Animation

Another Equally important decision, is choosing which poses you want to emphasize in order to get that reactive feeling when a character interacts with the world.

The collision stair case

The collision stair case

As stated on the previous post, updating all the pieces all the time was a bad idea. We needed to figure out a way to update only the ones that needed to be updated after another block got destroyed. The quick and dirty solution was to check all the pieces inside a bounding box on top of the piece that got destroyed.

About Amano & the collision conundrum

About Amano & the collision conundrum

So, a couple of months back, Mario and I were happily working away on The game, finding out the workflow and working out the kinks of developing for the PlayDate. We laid down the main mechanic, blocks were falling and colliding correctly the character was moving alright but we were doing everything on the simulator, NOT testing on the actual device. so when we decided to take it for a spin…  it crashed.

Pullfrog postmortem, Long Live Pullfrog 2-Bits

Pullfrog postmortem, Long Live Pullfrog 2-Bits

So towards the end of the year, Mario managed to get his hands on a Development console for the handheld "Playdate" and we decided to attempt do make a second version of Pullfrog, this time featuring a playful little crank and seemingly less restrictions except for the apparent ones like the black and white color of the screen. Oh the naivety.