SpinShot Devlog

Hey folks :wave:, new here, and to making games in general.

I manage a small product design team in my day to day life, but have limited experience with programming, outside of a few attempts to learn, which failed before I built anything substantial.

Anyway, cut to getting my Playdate about 6 weeks ago, and getting hugely inspired by the constraints of the device, and the ease of development, that I decided to jump in and have a go.

My first project is called (working title) ArcPong, and is as the title describes, a little paddle/ball/pong style game, where you control a little arc paddle in a circle 1:1 with the crank. It's a project I'm just making in order to learn the various aspects of Playdate development, as I'm not yet convinced that the core loop is that much fun (though I plan to iterate on it a bit).

CleanShot 2023-03-20 at 12.38.17

The templates were very easy to get started with even with near-zero knowledge, and I was very quickly able to get the basic arc paddle input working. The collision detection proved to be the biggest time-sink so far, and the approach currently is pretty basic with a few big downsides that I want to fix in future. If you don't have a background in math (I don't), probably don't start with something that involves so much trigonometry! XD I'm basically checking first for the distance of the ball from the centre to the distance of the arc (which is static), and then once it hits that, checks whether it's within the arc or not and either bounces back, or keeps going until it hits a screen edge.

So at this point, I start using GPT4 to help (I know, I know). Firstly, GPT4 is garbage for Playdate development specifically. It doesn't understand the Playdate SDK at all, so its code snippets almost never work unless it's a really simple change. What it is really great for though as a beginner, is it'll explain its suggestions and why it's trying something. So I'll say something like "Here's my code , but I'm seeing X unexpected behavior" and it'll be like "That's because Y, you could fix this by doing A and B", and A and B won't work, but it'll give me enough context about why the bug is probably happening, to be able to dive into documentation, this forum, stack overflow, to eventually be able to solve it in a similar way, or at least point me in a direction.

Anyway, after this weekend's work, I have a little working game with an animated cover image. Once loaded you Press A to start, and see a little countdown before launching into the game. The ball fires in a random direction, quite slowly, and each paddle hit increases your score, and the speed slightly. If you miss and the ball hits the screen edge, it resets your score and counts down again. The ball and fail sound have some placeholder synth sounds, and if you move the paddle before the ball hit, it adds a little spin to the ball which sends it in an arc which decays over time.

I'm posting here to kind of track my progress, and try to keep myself motivated.

My next steps are:

  • Show score on the fail screen
  • Implement top 5 high scores, saved between sessions
  • Improve countdown timers to allow paddle movement while counting down (right now implemented poorly with wait() as I couldn't get timers working properly)
  • Implement proper menu system with multiple actions
  • Put restart game into system menu instead of on B
  • Countdown when undocked/unpaused
  • Tweak spin values to feel more interesting

Thanks, look forward to continue to learn and interact with the community here a bit more :slight_smile:

9 Likes

Off to a great start. I'm impressed by the collision detection on the paddle, since it seems like it's doing more than just axis-aligned bounding-box collisions.

Think you could go into more depth about that implementation? I'm also working on something that involves collisions with arcs. I'm very new to game dev but have been learning the PlayDate SDK.

Yeah this is like iteration 5 or 6 of the paddle collision, and it still definitely has more to go. The first version was simply doing ballVelocityX = -ballVelocityX and reversing the X and Y velocity, but that proved incredibly un-fun as once you hit the ball, it'd just be bouncing back and forth in a straight line :smiley:

The current code for the bounce logic is:

if withinArc then
    if not bouncePerformed then
        local relativeBallPosition = (dirDeg - startArc) / paddleWidth
        local newAngle = (math.atan2(ballY - cy, ballX - cx) + math.pi) + (relativeBallPosition * 0.5)
        ballVelocityX = ballSpeed * math.cos(newAngle)
        ballVelocityY = ballSpeed * math.sin(newAngle)

        angularVelocity = crankSpeed * 0.008

        bouncePerformed = true
        hitPaddle = true
        ballSound()
        increaseScore()
    end
end

The bouncePerformed check was to stop a bug that happened when the ball found itself in the middle of the paddle, and just kept reversing as it'd never get out of the arc within a frame update.

I want to play with having other obstacles to bounce off, or "Stars" that you can hit for points, so I suspect I'll need a different approach to collision detection in future that's more generalised and not starting based on a static distance. That's a problem for future me though :slight_smile:

Wow, thanks for the explanation and snippet. I mostly am struggling with how to get withinArc since the sprite collision methods in the SDK only deal with rectangles. In your example the ball is well within the bounding box before it bounces.

Wondering if you are using some other collision library, faking it in some clever way, or if there are other ways of doing this in the SDK I'm just missing.

Sorry if this is turning into a stackoverflow thread. Like I said, I'm very new to this sort of coding.

Nice work! I really like the concept of adding spin and arc with paddle movement. I think that could definitely lead to some interesting gameplay, especially if the collectibles are on timers so you have to bend the path to reach them before they disappear, and perhaps increase a score multiplier for successive pickups.

Also, I’ve been working on a game with arc physics lately as well. I needed to support collisions from all sides so I cheated a bit and fully rounded the ends which means my nearestPointOnArcToPoint function suffices for detecting collision with a circle. I’m currently using vectors to perform my calculations, but wondering if there’s a faster approach.

@switchboard.op I believe withinArc means that the angle from the center of the circle to the ball is within the range of the start and end degrees of the arc itself, and the ball is at the appropriate radial distance from the center to collide with it. Bounding box collisions aren’t being used here, except perhaps as a precondition to perform the advanced collision calculations.

Ebs is exactly correct, I just measure the distance from the centre of the screen to the inside and outside of the arc, which are both static numbers. I can't use this process for collision with anything else, so I'll have to change it for anything else I add.

Ebs: I didn't have any success rounding the corners (I didn't try that hard) but would love to see a snippet of that code if you're happy to share.

Ah lovely. that gives me a new way to think about this problem. Thank you both for your replies.

Are you asking about the arc rendering or the collisions? For rendering, I did notice that the line width affects arc drawing but the endcap styles do not. I wound up just drawing circles at each endpoint of the arc with radius half the line width. As for collisions, here's my current function, which I implemented as an extension to arc itself:

function geom.arc:closestPointOnArcToPoint(p)
    local center = geom.vector2D.new(self.x, self.y)
    local v = geom.vector2D.new(p.x - self.x, p.y - self.y):normalized()
    local circleIntersection = center + v:scaledBy(self.radius)
    -- these next two lines feel sketchy and need more testing
    local angle = (360 - v:angleBetween(-Y))
    if angle > 360 and self.endAngle < 360 then angle -= 360 end

    -- constrain to the ends of the arc when beyond it
    local cw = self:isClockwise()
    local startAngle = cw and self.startAngle or self.endAngle
    local endAngle = cw and self.endAngle or self.startAngle

    if angle < startAngle or angle > endAngle then
        local midAngle = (self.startAngle + self.endAngle) / 2
        -- return the appropriate arc endpoint
        if angle - midAngle < math.pi then
            return self:pointOnArc(self:length())
        else
            return self:pointOnArc(0)
        end
    end

    -- return the intersection with the ray from arc center through point p
    return geom.point.new(circleIntersection:unpack())
end

I wouldn't be surprised if this has some bugs since I've only tested it in certain circumstances. I would be surprised if this was anywhere close to optimized in terms of performance, as I haven't spent any time attempting that yet.

2 Likes

A quick mid-week update. I've been checking off a bunch of my "next" goals for this week.

The following things are now implemented:

  • Show score on the fail screen
  • Implement top 5 high scores, saved between sessions
  • Implement proper menu system with multiple actions

CleanShot 2023-03-23 at 15.38.11

Still to do:

  • Improve countdown timers to allow paddle movement while counting down (right now implemented poorly with wait() as I couldn't get timers working properly)
  • Countdown when undocked/unpaused
  • Put restart game into system menu instead of on B
  • Tweak spin values to feel more interesting

New things:

  • Refactor scene management to be cleaner and easier to work with
  • Add a changeable setting, saved to disk
  • Improve synth sounds to be more pleasant
  • Improve button-prompt graphics
  • Add better "New high score" graphic
  • Implement a custom font

I'm experimenting with a different game mode, where I spawn a "coin" randomly, and if the ball hits it you get 5 points and the speed increases.It's a step in a more interesting direction, but there's a lot more experimenting here I want to do. I need a solution which allows for multiple coins, more precise ball control, and probably some mechanic that keeps it interesting if you keep missing.

CleanShot 2023-03-24 at 14.19.22

Some ideas:

  • Have every paddle hit worth -1 point, and every coin worth 5?
  • Have coin change position after 3-5 paddle hits if it hasn't been collected?
  • Possibly have the coins animate position over time, though no idea how much more difficult that'd make it for the player.

Got some collision issues I need to debug too. You can see that second coin should have been picked up. I might try the sprite collision for this instead.

Quick mid-week update. I got the coins working well, with animated images, and animations when they're hit. the AnimatedImage library has been a lifesaver. It's felt like progress is a bit slower as areas I am getting into are becoming a bit more complex, but I'm happy everything's working pretty well!

CleanShot 2023-03-29 at 09.43.36

Knocked these off my list (and some extra things that came up):

  • Put restart game into system menu instead of on B
  • Added Dark mode
  • Added FPS counter
  • Created basic settings menu system
  • Saving game mode high scores in separate lists
  • Add a changeable setting, saved to disk (Dark mode, Show FPS, Game mode)
  • Improve button-prompt graphics
  • Tweaked coin mode scoring, though still more work to do here.
  • Changed crank pause overlay to a dithered fade rather than an invert, to better allow for dark mode

CleanShot 2023-03-29 at 11.47.48

Todo:

  • Improve countdown timers to allow paddle movement while counting down
  • Countdown when undocked/unpaused
  • Improve synth sounds to be more pleasant (Might use non-synth sounds?)
  • Add better "New high score" graphic
  • Tweak spin values to feel more interesting
  • Tweak points values to feel more fair
  • Make a larger coin variant
  • Fix bug that causes ball to spin in a tight circle when near the top of the screen sometimes.
  • Allow viewing mode high scores without switching mode in settings
  • Make mode selection part of the new game flow, not a setting
  • Add a basic transition animation back to main menu
2 Likes

This is looking really good!

As an aside, have you played all of the season games yet?

Thanks :slight_smile: No I have not. I'm on the Executive Golf week so far. Hopefully I'm not building something that exists!

:grimacing:

well... not exactly.

Haha panic. All good, this is just for my own learning anyway, which it's definitely achieving. :slight_smile:

1 Like

(just looked forward and yeah, lol. It's ok! I'll either abandon this when I've learned enough, or look to ways to differentiate. Thanks for the heads up!)

Don't abandon it! Yours is sufficiently different and I'm excited to play it!

Sorry for the radio silence here the last few weeks! Despite Matt helpfully pointing out a late-season Playdate game with a LOT of similarity to my initial (frankly, not very original) concept, I've kept plugging away at it in my spare time, learning a ton. I still don't have the game in question unlocked on my device, but it feels like this concept presents a lot of problems, which have some fairly narrow solutions that I think I'm probably coming to independently of direct influence of that game, which is kind of interesting!

Anyway, what have I been up to?

Things from my previous list that I've now done:

  • Improve countdown timers to allow paddle movement while counting down
  • Countdown when undocked/unpaused
  • Improve synth sounds to be more pleasant (Might use non-synth sounds?)
  • Tweak spin values to feel more interesting
  • Tweak points values to feel more fair
  • Make a larger coin variant
  • Fix bug that causes ball to spin in a tight circle when near the top of the screen sometimes.
  • Allow viewing mode high scores without switching mode in settings

Animations
Up until this point, animation had been a bit of a mystery to me. I'd tried to get them working with image frame animations, and ended up using the AnimatedImage library someone suggested to get that working, which allowed me to kick the can down the road a little. Having said that, I'm a designer, and I needed some animation to keep me motivated, so I had a play with some intro animations on my main menu (which I realise I haven't posted here before). I also added some to the fail state, which I'm quite happy with when paired with its sound effect.

CleanShot 2023-04-17 at 20.57.25

CleanShot 2023-04-17 at 21.01.20

CleanShot 2023-04-17 at 21.11.24

Scaling up
Up until this point, I've had a single file, with a ball, a paddle, and a coin, and that wasn't enough building blocks to make a fun game out of. So I spent a bunch of time, initially refactoring my coin code to work as an array of coins I could place either manually or randomly. I experimented with having say 3-5 coins on screen, and when you'd hit them all, OR a 10 second timer elapsed, they'd reset to new random positions. This was an improvement, but the random positioning was a bit too random, and while I got the timers working, I wasn't yet convinced they were adding much yet. I built all this new multiple-coin logic out as a class, my first time using classes.

So I went back, and created a brick object that the ball could rebound off, also as an array, also as a class, but this time extending the sprite class (and learning about sprites as I went). This resulted in me needing to refactor my ball logic into a sprite class also to get collision working, and took a couple of weeks to get all this working right.

CleanShot 2023-04-17 at 21.20.48

At this point I basically ditch the idea of random placement, and instead build out a way to specify "levels" in a table, that I can author the positions of things. This was surprisingly straightforward, though probably needs a little more work to be performant, as there's a small framerate hitch when more than 3 items are loaded into the next level. I'll optimize this later.

CleanShot 2023-04-17 at 09.21.46

Scoring / Skill
So now I'm looking at ways to make this more interesting in terms of scoring, difficulty, etc. I experiment with adding 10 points when you hit a coin, and removing 1 point if you hit the paddle, to incentivise precision. I experimented with increasing the speed of the ball a little, but decreasing it again when you hit a coin, to have a similar push/pull on the difficulty and reward getting coins. I also tried a multiplier system, where getting two or more coins in a row would cause you to get a 2x, 3x, etc multiplier, with 1 allowed paddle hit between before resetting the multiplier on the 2nd paddle hit. I'm still to find a sweet spot between these levers that feels intuitive and fair, and I suspect it'll depend on the levels, and if I add other types of objects to the play field later on, but I'm reasonably confident these should be easy to tweak and maintain as I go for the time being.

Precision
If I'm incentivising precision, I need to offer tools to control that precision. Previously I'd added "spin" to the ball when the paddle was moving during a hit event. This felt OK, but had some bugs in the implementation I had to kind of hack around, and wasn't written in a way that was very easy to experiment with. It felt OK most of the time, but it wasn't enough.

So I thought, what if there was some kind of power-up that gave the paddle a magnet? Maybe the ability to pull the ball towards the paddle for short periods might open up some interesting skill challenges? So again, I completely rewrote the ball movement system to allow me to do this, but I got it working yesterday evening and it feels pretty good! I have some animations in, a power bar that has the option to either fill slowly over time, or on hit events such as picking up a coin. Will need tweaking to figure out what feels best. In that rewrite though, the spin code on paddle hit was lost, so I'll be re-implementing that on top of the new code in a bit.

CleanShot 2023-04-17 at 21.26.52

The name & the future
I'm getting to a point where I might think about releasing this in a couple of months, so anything with the name Pong in it isn't going to fly if Atari has any say! So I'm brainstorming some new names and should have an updated title card/intro next post :slight_smile:

I also have a question if anyone's able to point me in the right direction. I had an attempt to convert the existing coin class to a sprite also, but the AnimatedImage library didn't seem to play nicely with being popped into a sprite's setImage function. Is it possible to use this library in conjunction with a class extending a sprite, or is there another really simple way to pop an animated image into a sprite? Thanks :slight_smile:

5 Likes

After a bit of a struggle I managed to re-implement the paddle ball spin code in a way that maintains the magnet functionality. The switches between levels dropping frames was also a problem given the speed of the game, so I did the first bit of optimization, managed to get all my old gfx.clear() functions removed, optimized the background redraw, and changed the coin and brick functions to not be defining individual animated images (eck), which helped enough that I'm going to leave it there for now. All in all, took less than 30 mins!

I also implemented a hacky level viewer that lets me step through the levels without the ball/gameplay initialized, which is helping speed up level iteration a little.

Now the fun part of figuring out what types of levels are fun vs crazy-hard.

I'll probably experiment with two other types of bricks, one that degrades and is removed after multiple hits, and another which kills you (or removes score?). I've been holding off on that second one as I think I'll need to implement a lives system, or it'll be too hard. I'll also probably need to tweak the gameplay a little to have the ball start on your paddle instead of the centre. Having obstacles change when the ball is mid-flight can also give some gnarly results if a brick appears in the current trajectory. Not sure how I'll solve that but that's a future me problem :slight_smile:

2 Likes

Looking better and better. I particularly like how dynamic the animations are!

1 Like