SpinShot Devlog

(The game previously known as Arc Pong) has a new name and logo! :slight_smile:

CleanShot 2023-04-22 at 14.16.01

3 Likes

NICE! Loving the Memphis Design inspired wrapping paper.

Keep going, I'm really excited to play your game.

I'm really happy with how this thing's shaping up this week.

Ball positioning
So first thing's first, my assumption about the ball firing from the centre being bad design that I'd need to fix was totally founded, and I refactored the core game to start with the ball attached to the paddle until the player fires it (it fires directly toward the centre). This actually meant I could get rid of the countdown before the game started, and after the user died, so I've ripped out and simplified a bunch of that, and getting into and out of a game feels snappier now.

CleanShot 2023-04-27 at 16.59.37

Lives
The next big bit of feedback I'd had from some friends was it was just too hard, and brutal that you died and restarted the game each death. When it was a score-chaser with randomly positioned coins, this was fine and made sense, but as soon as I switched to the concept of levels, it was a bit rough. So I've implemented a basic lives system, 3 lives to start, and dying restarts the current level. You can back out to the menu after any death if you want, but I'm just showing the final score and stuff on the slightly tweaked new game over screen.

CleanShot 2023-04-27 at 16.44.43

I have a bunch of improvements I already want to make to this, but just animation/polish stuff, nothing blocking, so they can be fun tasks for when I get stuck elsewhere.

Block types
I implemented "sand" blocks that disappear after 3 hits. I made the blocks displace a little when they're hit to give it a bit of weight, but I also want to draw an animated sprite on top of the sand blocks when they decay, but am having trouble implementing that so far. I've got a few other ideas for other obstacles and special blocks, but nothing implemented yet.

CleanShot 2023-04-27 at 16.47.05

Multi-phase levels and quicker iteration
Next up, after quickly building about 20 levels to test with, I realised two things: I'd specified the levels with numbers like "level_1" and it was a huge pain to iterate if I decided to add or remove a level. I was also finding the levels a bit rigid, so I completely redesigned the levels system.

{ 
    {levelID = "L3B2D4"},
    {type = "brick", x = cx-48, y = cy},
    {type = "sand", x = cx+48, y = cy},
    {type = "coin", x = cx, y = cy-48, phase = 1},
    {type = "coin", x = cx-72, y = cy+24, phase = 2},
    {type = "coin", x = cx+72, y = cy+24, phase = 2},
    {type = "coin", x = cx-24, y = cy+48, phase = 3},
    {type = "coin", x = cx+24, y = cy+48, phase = 3},
}

Now, a level is split into an arbitrary number of phases. Objects can be loaded in at the start of a level, and persist through all phases, or can be loaded in per-phase. If you die during a phase, the whole level starts over. Getting all the coins in one phase will proceed to the next phase, or next level. I implemented this on top of the existing level structure so that all my current levels would still function as single-phase levels for the time being.

CleanShot 2023-04-27 at 16.53.46

Level viewer
As I mentioned in the last update, I'd made a really simple "level viewer" function which basically just hides the ball and paddle, and allows me to step through phases and levels one at a time just to visualise placement and position a little easier.

CleanShot 2023-04-27 at 16.50.28

Metrics & difficulty
My current challenge is that now that I've got a bunch of levels designed, I'm finding it tricky to figure out which ones are difficult, which ones are easy, and what the order should be (or if I should drop some entirely). To try and help figure this out, I'm implementing a logging system which will calculate averages per level/phase on different metrics such as time spent, paddle hits, deaths, etc, and write those averages to a file on game over, grouped by a unique ID per level. After gathering some initial data, I'm going to use that data to figure out some simplistic weighting metrics to come up with a numeric "difficulty" score, and try ordering by that.

local metrics = {
  levels = {
    {
      levelID = "L3B2D4",
      timesPlayed = 0,
      avgTimeSpent = 0,
      avgDeaths = 0,
      avgPaddleHits = 0,
      avgScoreIncrease = 0,
      phases = {
        {
          phase = 1,
          timesPlayed = 0,
          avgTimeSpent = 0,
          avgPaddleHits = 0,
          avgDeaths = 0
        },
...etc...
}

That's it for now! I'm trying to get as much of the core design done by this weekend, before b360 unlocks for me on Monday and invades my design brain.

4 Likes

Some wins and some losses the last few days. Here's a quick update.

Moving objects
Another pretty large refactor of the levels system, I can now define a string of coordinates for an object to move along, each point with an optional wait time and speed.

CleanShot 2023-05-02 at 10.09.15

{type = "coin", x = cx-24, y = cy-24, phase = 2, path = {{cx-24, cy-24, 2}, {cx+24, cy-24, 2}}, speed = {0.5, 0.5}},
{type = "coin", x = cx+24, y = cy+24, phase = 2, path = {{cx+24, cy+24, 2}, {cx-24, cy+24, 2}}, speed = {0.5, 0.5}},

Moved the levels out into their own separate file, so they're a bit cleaner to manage.

Ice blocks
I was able to fairly quickly create a type of block I'm calling Ice, which takes 2 hits to destroy (rather than 3 for sand) and contains a coin that spawns in its place when destroyed. Having hits and bounces be more closely tied to progressing through a level seems to make for much more satisfying gameplay, and makes the levels feel a little less "puzzley" (which is good). This block type was shockingly easy to add given how the other types were built.

CleanShot 2023-05-02 at 10.10.57

It should be pretty straightforward to add Ice blocks that hold other types of objects in future, such as power-ups, extra lives, etc.

I need to improve the effects on hit for both these and the sand blocks. I want some kind of animated effect on degrade, and destroy for the ice and sand blocks. Also want to figure out how much I can get away with in terms of animating new objects in in a more polished way. More levels first though.

Delta time & debug mode
By far the biggest change was changing my code from being frame-based, locked at 30fps, to using delta time and unlocking the framerate. There was a lot of trial and error here, but mostly everything's working pretty well. I've upped the default framerate to 50 and it seems to be running well on device at that speed, which is great. The main reason for shifting to delta time though, was the ability to much more easily add abilities like slowing time. I've added a speedFactor variable to the delta calculations that let me easily slow down or speed ramp time in the game, as well as pause more effectively.

As the changes affected how the physics of the ball (spin, decay, etc) were calculated, I added a debug mode that visualised ball direction and historical path. I've got this on a hacky toggle for now, but it's been incredibly useful so far.

CleanShot 2023-05-02 at 10.27.16

The lowlights
Collision works mostly pretty well, though there are still a couple rare instances where collisions can bug out when a moving block hits the ball in a certain way.

CleanShot 2023-05-02 at 10.22.01

I have a very slight cooldown where one block can only trigger a collision once every 50ms, but I may tweak these values a little and see if it helps. Not yet been able to reproduce the problems consistently though.

Level metrics
The biggest loss this week was that my plans for level metrics that I mentioned in the last post, I've abandoned for now. I'm going to spend a few weeks on just content creation and trying to get a bunch of levels I'm happy with, now that I have a few more tools in the chest to play with.

b360
I finally got this unlocked toward the end of last night. Super slick production, hats off to everyone at Panic who worked on this. Love a bunch of the decisions they've made (crank to start, chef's kiss), beautiful animations. There's a bunch of things that are shockingly similar in our implementations. Our coin animations are very similar, and block types have a lot of similarity (though let's face it, they're blocks in a breakout game, there's only so much you can do).

To be honest though, I'm feeling better about how different SpinShot feels to play moment to moment, mostly with 1:1 paddle input, and the ball spin, which I find really satisfying. Unlikely to get a spot on Catalog, but I'll be stoked with a modest itch release and 10-20 players at this point :smiley:

4 Likes

You should totally still submit to Catalog.

This dev log is really great and the game is shaping up super nice.

Judging by the GIFs you've got to be pretty good at your game, which can be both a blessing and a curse for level design and difficulty grading.

Appreciate your enthusiasm Matt! At the moment I'm not expecting the game to be too brutal, and for the difficulty curve to be more in high-score-chasing, but you're right that difficulty grading is going to be tricky.

Haven't quite decided on whether there should be a meta progression layer, or some sort of overarching structure outside of just one level after the next.

I'm deep in content creation mode now and it's definitely a bit of a slog, though the playtesting is still fun :slight_smile:

3 Likes

Hey! I just want to echo matt's sentiment that your game is shaping up really nice and yes, still apply for Catalog! b360 is a thing, but your game IS already different enough!

Also please add the dithered trail effect to the ball :comet: :comet: :comet:

Good luck! If you'll need a tester - hit me up :wink:

1 Like

Appreciate the kind words :slight_smile: I need to fix my current performance bottlenecks before I think about dithered trails, but I agree a trail would be good! Helps visualize the spin better.

2 Likes

What are your specific performance bottlenecks?

I was going to hold off until I'd had a proper crack at fixing them (thanks for your help in discord, pointed me in the right direction).

Performance
Basically I have the ice, brick, sand, and ball implemented as sprite classes, using the sprite collision and that's working great as far as I can tell.

The coins however, being perpetually animated, and being the first thing I implemented, are a class, but not implemented as sprites. I couldn't get Dustin's AnimatedImage playing nicely with sprites. I had a first attempt at implementing the animatedSprite library from this forum yesterday, but got totally stuck with implementation issues, so pivoted off onto other problems for now. (was trying to get it working with loading the imagetable in my :init() function, rather than passing it as a parameter, which I'm not totally sure it supports? but anyway...).

When the player completes a phase or level, all the objects and coins would change to the following phase/level, but A) it could be quite jarring to have objects change immediately and have no time to react to them, and B) the ball could end up in the same area where an object needed to spawn, which would be a physics nightmare.

My solution was when you collect the last coin in a phase, the game slows down (I need a better visual effect and affordances to indicate this is happening/about to happen), the previous level objects are unloaded, the new level objects are loaded in one by one with a slight time offset, and then the game ramps back up to full speed again. This means the ball almost never leaves the area the last object was in before the next level is loaded, which is great.

CleanShot 2023-05-08 at 12.18.51

The problem is that loading in the objects 1 by 1 is causing the framerate to dip way more than it was when I was loading them all in at once (happens on device, not really in simulator, which is why you don't see it dip here). There's a bunch of table inserts and removes at play (I keep separate tables for each type of object, which may be an issue), and I think that's compounding with a bunch of draw calls using Dustin's animatedImage library, which I'm using for the coin animations.

So I basically want to refactor all the coin code to use animated sprites, and see how much of the issue that solves. I suspect most of it, but it's a big change that makes the game unplayable in that state until I get the sprites working, so a bit rough for motivation when I hit a roadblock :slight_smile:

It's not a huge issue at this stage, but I'm trying to hit a solid 50fps and this is dipping to around 43 on level load. The speed ramp negates any actual gameplay impact, but I want to do things like animate in each object nicely, rather than just have them pop in, and I won't be able to do that without more overhead to work with.

Ball physics and first playtests
So unrelated to the performance stuff, I had some friends playtest the game for around 10 mins each over the last week. There was a bunch of interesting feedback that's actionable, and some that's probably not that indicative, as they were also totally new to the Playdate as a device, so there's probably some familiarity with the crank and holding the device that I'm assuming will be a little easier for most players.

Anyway, one of the bits of feedback was interesting. A particular level I'd designed specifically to teach the player how to curve the ball, either with spin, or via the magnet. I start it with 2 blocks with a gap. The second phase closes the gap with a breakable sand block, and the third closes it off with a brick, forcing them to go around the wall to get both coins.

CleanShot 2023-05-08 at 12.24.30

The way the ball physics is implemented here, where the ball hits the paddle affects the angle it bounces at. If it hits the middle of the paddle, it bounces straight toward the centre, assuming no spin. If it hits towards either edge, it adjusts the angle up to 15 degrees off that centre point. This means it's actually quite tricky, if not impossible, to get around this wall without spin.

The feedback was that the player expected the ball to work closer to how an actual bounce would, in that the incoming angle was reflected to the outgoing angle (as it does when it hits bricks). I've played so long at this point that I hadn't actually given this much thought, so I adjusted the code to add a proper physical reflection angle. The game immediately became way less fun (to me, anyway), and way more chaotic. It became extremely easy to get into a situation where you were just bouncing the ball around in a triangle or square, missing the objects in the centre entirely, and at a certain point it becomes incredible tricky to adjust back to where you want the ball to go.

CleanShot 2023-05-08 at 12.15.53

It also made the spin mechanic even more riskier than it was already, as adding a little too much spin would end up being catastrophic.

CleanShot 2023-05-07 at 11.51.55

I've tried a few things to make the more realistic one a bit more playable, like constraining the maximum angle to somewhere between 90-100 degrees, but it still feels pretty wonky. I'm on the fence about whether to action the feedback, or stick with the unrealistic, but predictable model and ignore the feedback. I know it's hard for people to really comment here one way or another without hands-on with it, but just documenting my thought process here really as others may find the design messiness interesting :slight_smile:

I imagine this is the problem developers of space games have when people ask for realistic space physics. Just SUPER easy to get your ship into an unrecoverable spin.

1 Like

I look forward to finer grain performance optimisations! Above is quite high level.

With regards to your paddle collision: I say keep your original fake collision and avoid the situation through level and uxdesign.

Yeah that's what I'm thinking too. I've got the different modes on a setting switch for now, as it's only one function that needs changing, so I can continue to experiment, but not block other tangents I might go on :slight_smile:

Here's how the level changes look in the sampler...
CleanShot 2023-05-08 at 14.02.15

Here's the call tree for the middle spike. (the larger one to the right compounds with some garbage collection, though I figure that's a bit easier to manage and might be a non-issue if I can fix the other spikes).

i_have_no_idea_what_im_doing_dog.gif :wink:

2 Likes

Just want to be another voice of encouragement. This looks really fun, can't wait to play it!

2 Likes

Performance optimization
I've called time on this for now. After getting frustrated by the animated sprite difficulty (the SDK needs easier to use animation functions in future IMO), I tried an experiment just implementing static sprites for coins, to see whether it had a positive performance impact. While I saw some improvement from these, ultimately there were still spikes happening on the level change. Given it was not impacting gameplay much, I decided to move on to the other things below (but I'll circle back on this at the end).

Lives
I worked on adding a bit of animation to getting or losing a life. Initially I wanted something much flashier, where the lives would be physical balls that would kind of curve into position in front of the paddle after losing one, but I ended up settling for some simple animation that drew the eye a little more instead. I'm not totally happy with these but they're good enough for now.

CleanShot 2023-05-10 at 09.40.52

Visual effects
Something I've been wanting to do for a while is for the destructable bricks to have some kind of effect in addition to the displacement that sold the impact better. I finally implemented a simple effects function that I can pass the name of an animated image, and an x/y position, and it'll add it to a table, draw it to screen, and then remove it. I can spin up a bunch of these on hit events etc and it ended up being pretty straightforward. Currently there isn't much variation, but again it's good enough for now that I can move on.

CleanShot 2023-05-10 at 09.58.23

CleanShot 2023-05-10 at 10.33.31
(Ignore the FPS here, for some reason Mirror nearly halves the framerate)

I also experimented with animating the coins in using another animated image. I was having some issues applying the animation offset, but with all the coins animating in at once, I discovered the performance had increased massively. I think initially I assumed that having bricks and coins animate in over multiple frames would sort of spread the per-frame performance cost, but actually it seems like the opposite is true. I put a placeholder animation that was just a reverse of the explosion for now, but it actually looks alright. I'm going to experiment with animations for the other object types as well, maybe a simple dither fade to start, and remove all the offset adding code.

Next: Levels & Tutorials
So now that I'm happier with some of these basic systems, I'm going to shift my focus to two areas. Firstly building up to around 50 multi-phase levels (I have about 17 at the moment), and adding some sort of tutorial to the game.

I'll probably approach the tutorial in different ways, first with a basic slideshow with some animated gifs and text that can be viewed at any time. That'll be MVP, then I can look at ways of adding in tutorials in-game. Ideally I'd like as much as possible to be just encouraged by the level design, and not have to resort to popups and things, but spinning the ball well is a tricky thing that I've seen isn't easy to discover, so we'll see. Should be ready for a little playtesting after those two things are in place. :wink:

3 Likes

I really love your visual effects. Your game really feels lively and fun to play.

1 Like

Just a quick update before the weekend!

Tutorials
I actually skipped the static tutorial slideshow I thought would be easier to implement, and actually just went straight to an inline system. I can call these tutorial bubbles (which are just an animatedImage) from anywhere, the game pauses, and the box will move out of the way of the paddle so you can see when you're doing for when you continue. I think they look great, but I'll get some playtesting feedback from some friends next week.

Bonus is in order to get these working when someone picks up a coin, I've now added a generic function to be able to trigger events on pickUp of coins. I'll use this for ability pickups and stuff probably.

CleanShot 2023-05-12 at 11.16.12

New levels
These will take a bit longer, but I've tried to speed up my iteration a little bit by loading up Figma (UI design tool) and being able to rapidly place stuff on a grid there to visualize level ideas better. Can't playtest them there of course, but it's definitely helping. Doesn't feel worth it to build an in-game level editor at this stage.

Once I have my ~50ish levels, I'll playtest them obviously, but I'm also thinking about how to break them up into chapters or something, to allow people to practice more advanced levels (even though for high-score chasing, starting from the start will be of most benefit).

I'm thinking different themes for each chapter, changing the background graphics and making them more rich and interesting (as there's quite a bit of unused real estate to either side of the play area).

Also been making an Itch.io page, which makes this whole thing feel way more real! I appreciate everyone's encouragement and support :slight_smile:

Making your first game part-time, solo, is a weird and kind of lonely thing :smiley: Enjoying it though.

6 Likes

This is wonderful!
I'm joining the others that encouraged you to keep it up!

2 Likes

I'm slowly building things which facilitate removing other things I've built (which I guess is good, feels like refinement.). Dark mode is being killed. It was a hacky XOR rectangle over the entire screen, served its purpose of being a setting while I built settings, but as I develop new features it makes less and less sense.

Game Speed
When I migrated my game to use Delta Time, I'd stupidly baked game speed into the delta time calculation. In my brain I was like "Hey cool, I can slow down time by modifying a multiplier!" and implemented that in a bunch of places, but eventually had a gnarly bug where my game would never actually slow to a stop. I finally realised it was because my speed ramping was based on DT also, and it would effectively elongate forever as it's animation tried to get to the end, but time kept on lengthening.

So I refactored it again to have two speeds, a gameSpeed and a dt that I could use for timings regardless of game speed. Cool. I THEN spotted a duplicate sprite update being called in my draw code. I realised this meant I'd been calling sprite updates twice per frame, which was not only doubling my game speed, but halving my performance. It was a fairly easy fix, but I had to re-tweak all my speeds and stuff that were based around the game 'feeling' a certain way at a certain speed. There was a stopwatch, and simulator/device triggering things at the same time involved :slight_smile:

Chapters
I've been tugging at the thread I mentioned last post about theming chapters of the game. I've built out a version of this that is working, but is a little brittle, and needs some additional polish. I've got some proof of concept art in there for a tutorial space theme, a desert planet, an ice planet, and an industrial factory, all of which will tie into new mechanics being layered into the levels.

CleanShot 2023-05-17 at 13.55.28

CleanShot 2023-05-17 at 13.56.01

CleanShot 2023-05-17 at 13.56.29

CleanShot 2023-05-17 at 13.57.09

The thing I learned from my background swap code, was put that stuff in an object! I had a bunch of attempts here to try and get the screen clearing effectively on swap. Tried gfx.clear() in the callback, outside the callback, and they either didn't work, or had huge performance hits due to being called every frame. The approach below seems to have ticked all the boxes though, and is working nicely.

function setBackground(background)
  if bgSprite then
    bgSprite:remove()
  end
  bgSprite = gfx.sprite.setBackgroundDrawingCallback(
    function( x, y, width, height )
      gfx.setImageDrawMode(gfx.kDrawModeCopy)
      background:draw( 0, 0 )
    end
  )
end

Dark backgrounds
The space level being a dark background has uncovered a huge number of headaches in terms of visual design of the objects that I'm slowly plugging away at solving. For now, I've settled on variants of the coins, ball, and paddle, because with the current design, the space levels only include coins. I'll solve the blocks later once I have a more scalable approach I think.

Sitting at around 29 levels at this point. Still motivated and feeling good!

4 Likes

Great progress! New screens look great, I'm intrigued at the scenarios and what will come to level design.

See playdate.display.setInverted(flag)
https://sdk.play.date/inside-playdate/#f-display.setInverted

1 Like

Good to know for future, thanks! The addition of black backgrounds made it make no real sense now anyway, so maybe a future project :slight_smile:

At the moment the chapters are themed around the types of blocks I already have. Sand for desert, ice for ice, and moving blocks for factory.

Depending how ambitious I'm feeling I have some other ideas I want to prototype though, let's see what happens!