Astro Jump

Astro Jump at its core is a typical endless-runner. You walk, jump and slide to avoid obstacles and try to get as far as possible.

What makes it interesting is that it all plays in a widget on the iOS/iPadOS home-screen.

Making a game in a widget brings a whole set of challenges and limitations, e.g. many SwiftUI features not being available. While this means it's not that easy to build a game for a widget, it also makes it way more interesting.

Since iOS 17 widgets can be interactive. So until 'Astro Jump', I didn't make an interactive widget and I was checking out existing ones to see what's possible. I stumbled upon some 'games' too. But I was a bit disappointed because most of them were not really playable (limited interactions). And also everything was crammed into a small widget. So my goal was to build something which improves some of these issues but I wasn't sure yet how to achieve this.

Then it hit me: How cool would it be to have a separate widget for the screen (where the action plays) and one for the controls, some kind of gamepad! Not only hasn't this been made before (as far as I know), it also solves the issue with limited space and it makes a game more playble because your fingers won't cover the actual gameplay.

Basically I wanted to see if it's possible and I started to make some quick prototypes. In the following I want to go into the different aspects and challenges of a game in an iOS widget:

But let's start with the most interesting aspect of Astro Jump. The one that sets it apart and probably the reason why you're even reading this.

Two widgets for one game: Gamepad and Screen

Astro Jump

When I started with Astro Jump I set myself a timeframe of two or three weeks. And I thought this point about one widget controlling the game in another widget would be my main challenge.

I experimented with a few ideas, some wild ones, but they didn't really work. I thought I needed to have two different widgets, different types ('kind' it's called in iOS widget language). And then one refreshes the other. But I didn't have much luck with this. So I played around with system iOS widgets and tried to understand how they behave. I also added multiple widgets of the same kind.

Then I noticed that if I add multiple widgets of the same kind, they all refresh together. You can try this by adding e.g. two Reminder widgets showing the same list and if you check off one item, it disappears on the second widget too. After I understood this it was almost too easy and worked right from the start.

So my idea was the following and it's actually how it works now in Astro Jump:

  • There's just one widget kind, one widget view.
  • It comes in two size, small and medium.
  • And depending of the size it will just return a different 'subview' - for gamepad or screen.

The pseudo code of the widget view struct would be something like this:

struct GameWidgetView : View {
    @Environment(\.widgetFamily)
    var widgetFamily
    ...
    var body: some View {
        if widgetFamily == .systemSmall {
            //Where the buttons are
            
            GamepadView()
        }
        else {
            //Where the action takes place
            
            ScreenView()
        }
    }
}
Astro Jump

It's really that simple! And as you'll see in another section, when you press a button in a widget, it fires an event and you can do some stuff and then it reloads the widget(s) of this kind. And since the screen and gamepad are in the same widget (but only one is visible) this setup 'just works'.

The point I was afraid would be the most difficult and maybe even impossible one, turned out to be simple and I had this working in a few hours while losing endless time in other parts like animations.

How interactive widgets work in iOS

So now we already saw how the feature with separate widgets was done. Let's take a few steps back and explore how interactive widgets work in iOS 17 before we dive into the specific challenges of making a game in a widget.


Astro Jump

In general that's how it works. You have some kind of button in your widget, this takes an AppIntent (which is a struct performing an action). After performing the AppIntent the widget gets reloaded (well it's a bit more than that, depending on your implementation, but basically it's reloading the widget).

Now if we take this approach and apply it to Astro Jump, for example for the 'Jump' button, then we have something like this:


Astro Jump

The Jump Button has an AppIntent called JumpIntent, this performs when the button is pressed and you can run some of your logic. In my case I use it to update a singleton (yes, I know!) where I store my game state. For example what is the current action and in this case it's 'jump'.

Then the widget gets reloaded and there I can access my GameState and I'll know if the player pressed jump or something else.

Make it jump!

After understanding how interactive widgets work, it's time to bring some action to Astro. We now know what happens when the player presses a button and that we can access the game state in the widget view.

In a widget your possibilities of animating are very limited. You can just use some default transitions (maybe there are some exceptions, but let's assume this). Transitions are animations when a view appears or disappears.

So how do you make something like a jump animation when all you have is view transitions? That was my question as well.


Astro Jump

I have two views of our player 'Astro' in the widget view. But only one is visible, depending on the GameState information which tells me if jump or walk has been pressed.

At the beginning of the game we're in the 'walking' state. Now when the user presses jump, remember the widget gets reloaded and we have this information in the view, we hide the walking view and show the jumping view. This way we can use animations to transition between them.

It's the easiest solution I found for this problem, because something simple like 'animate this view a view pixels up and then down again' seemed not possible for me to achieve in a widget. While it would be very easy in a normal app.

This solution has some small problems but it's pretty good and really ok for a small game like this.


Astro Jump

The issue is, we can only animate the 'down fall' of Astro. So he is instantly in the air without an animation but then falls nicely down (animated). When in the game and in action you almost won't notice this, unless now, because now you know it and you can't unsee it! Sorry.

Pseudo code:

if gameState == .jump {
    //moves astro down
    //from 20px above
    //when a jump occurs
    
    jumpAstroView
        .transition(.asymmetric(
            insertion: .offset(y: -20),
            removal: .identity)
            )
}
else {
    walkingAstroView
        .transition(.identity)
}

Scrolling illusion

Another interesting thing to achieve in a widget game is the scrolling. As we now learned, widgets are pretty simple and react to a button press where it reloads the widget. It's really not that dynamic if you think about it.


Astro Jump

In Astro Jump I have different types of blocks. Let's say they're numbered. So the playfield is an array of blocks (types) something like [0, 1, 1, 0, 2, 4, 3] where every block type is a different kind of obstacle, or no obstacle etc.

Now all I do is at every reload of the widget, I remove the first block and append a new one. And when you animate this transition, it looks like it's scrolling. The player (Astro) actually never moves.

//In the intent which fires on button press
//remove the first and append a new one
PlayField.update()

...

//Then in the widget
HStack(spacing: 0) {
    ForEach(PlayField.blockTypes) { blockType in
        viewFor(blockType)
    }
}

When I generate a random new block to add at the end, I make some checks that the game stays fair at all times. For example if the previous block was something like 'lava on the floor', then the next one needs to have a solid type of floor.

Using Kolibri to design SwiftUI-Views

Astro Jump

Astro Jump was 'born' because I wanted to take a little break from my main project 'Kolibri'. It's an app which automatically generates SwiftUI code from designs and animations. You can draw in it like in a vector app (Sketch or Figma for example) and in the included animation editor you can bring your designs to life - and again, you have the SwiftUI code automatically.

So when I thought about the UI for Astro Jump I wanted so nice retro stuff with lots of gradients and shadows. And I didn't even plan it this way but it was the perfect use case for Kolibri. So I drew the buttons, the screen and all the UI you see in both widgets in Kolibri. Multiple fills, gradients, shadows, blend modes. It was actually really cool and even helped me to find a few bugs which I didn't notice until then.

Here you can see the design of the screen. At any time you can just copy the generated SwiftUI code.


Astro Jump

The sprites like the background, the tiles and Astro itself are actually bitmaps. Which for such things makes sense and has good performance. They don't need to be vectors. Funnily I drew them in Sketch (Website), which is a vector drawing app. But it went really well, I decided on a size for a square which then would be my 'pixel' and I duplicated those squares around and colored them (I specified a palette of 5 retro green tones to use from).

Kolibri is available for iOS, iPadOS and macOS. Of course it's probably best on the mac but event the iPhone version has all features. You can read more about it or download it here:

Kolibri Website

App Store

Notes

I didn't cover everything about Astro Jump in this post. There are other things like how to detect if the player 'died' or if the time ran out. But I decided to write about the most interesting and challenging topics.

If you have questions or want to know more about another aspect of this game or just say 'hi', the best way to reach me is on Mastodon or via the contact form on my site. I'd be glad to hear from some of you and I hope this post was interesting and maybe it gave you some new ideas or you even learned something.

Thank you for reading, Sandro.

And of course you can find Astro Jump in the App Store here