Skip to content

Polishing and Publishing

In two days, it will have been an entire year since I committed the first lines of code for this project. The game isn't groundbreaking, innovative, or even particularly complex, but it's the most complete personal project I've worked on in well over a decade. For the first time, I can see a path to releasing a complete game before me.

There are a handful of things I still need to tackle - some of them in the code, while others are more logistical. Here's an easy one: following Apple's best practices, I'll set the AVAudioSession's category to AVAudioSessionCategoryAmbient so that the game's audio is muted based on the state of the silence switch (which doesn't even exist on the latest iPhone 15 Pro). As much effort as I've put into the game's audio, the unfortunate reality is that most people will mute the game anyway.

21.1 Game Options

Using the "ambient" audio category rather than the "solo ambient" category means that the player can play their own music in the background while playing the game, if they so choose. In order to make this more of a possibility, I'd like to provide options for the player to mute the music and sound effects separately.

I created a new OptionsScene to support this. Up to this point, all of the scenes have been entirely composed within a constructor inside a header file, but since the TitleScene and OptionsScene refer to one another, I'll need to split the constructor definitions into their own .cpp files. This is objectively better, so I'll go ahead and give the same treatment to the InfiniteRunnerScene and GameOverScene. I'm pretty sure I left a bunch of #includes that were no longer needed anyway, so I can take this opportunity to clean those up.

It was weird having the title screen music restart whenever the player navigated back from the options, so I added a new getCurrentSongType() method to the AudioManager interface so that the TitleScene would only restart the song if it wasn't already playing.

I spent some time building out a new ToggleSystem, which queries for entities containing a new Toggle component. The system dynamically creates Buttons with shared click handlers that toggle an internal boolean state and fires a new callback, which is provided with a boolean parameter representing the new state. Nothing too complicated, just a lot of code to keep the button positions, sizes, and colors consistent with the current state.

Using the new Toggle component, I was able to easily whip up some switches for enabling and disabling the game's music and sound effects. While I was at it, I decided to add a third switch to disable screen shake, for those sensitive players out there.

I updated the SaveManager and its various implementations to contain methods for updating and retrieving the enabled state of the three toggles. I wired up the initial state of the toggles with the current state of the SaveManager, and wired up the callback to update that state.

I started to wrap every call to the AudioManager's play() methods with the appropriate check against the SaveManager, but I decided instead to add separate enabled/disabled states for music and sound effects to the AudioManager interface. This way, the scenes and systems can just call whatever play() method they want, but it will only play the sound if it's actually supposed to. The toggle callbacks update the state of both the SaveManager and the AudioManager, and the Engine class now sets the state of the AudioManager in its constructor, based on the current state of the SaveManager.

The screen shake toggle is only slightly different. Rather than storing any internal state, the ShakeSystem just checks the state of the SaveManager in its update() method and returns early if it's disabled.

The code changes can be found here, or you can play around with the options.

Options Scene

21.2 The Elusive First Frame

Every time I play the game, I can't help but notice a single frame at the beginning of each scene in which all objects are rendered in the center of the screen, seemingly without the correct view or projection matrices. You can even notice it in the screen recordings throughout this series!

I've looked a bit for anything obviously wrong that might cause this, but I never wanted to spend much time on it. Now, in the "polish" phase of the process, I suppose I should try a little harder to figure it out.

Thanks to my handy dandy debugger, it wasn't all that difficult to step through the code to figure out what was going on. As it turns out, it both is and isn't the first frame of the new scene being shown on the screen.

Let him finish!

Evidently, it is both the first frame of the new scene and the last frame of the old scene. Composition of new scenes takes place within their constructors, including the allocation of new Cameras and Renderables! When the player presses a button intended to transition to the next scene, the new scene is constructed, thus all of the visual elements of that scene are allocated within the Renderer. However, the construction of this new scene takes place within the update() phase of the current scene. After all of the systems of the current scene have finished their update(), the Engine calls the Renderer's draw() method, which renders all of the Renderables that currently exist, which includes those from both scenes!

The reason the elements of the new scene appear front and center (and without a proper viewProjectionMatrix) is because actual scene transition hasn't taken place yet, thus the update() phase of the new scene was never executed, so the CameraSystem of the new scene never had a chance to construct the new viewProjectionMatrix.

That was wordy, but I hope you get what I'm saying. Basically, I just need to delay the allocation of the new Renderables and Cameras until the scene transition is ready to take place.

I ultimately solved the issue by just introducing a new init() method to the Scene interface, which the Engine doesn't call until it's time for the new scene to be loaded. Since the ServiceLocator was normally passed into each scene's constructor, this new method doesn't automatically have access to it, which obviously broke some of the logic requiring engine-level services. I decided to cache the ServiceLocator reference inside of the Scene base class, and added a new get<T>() method to the class that just forwards its template parameter to the ServiceLocator's version of that method, so now each scene can grab any engine-level service as needed.

After making those changes, however, I was still noticing some weird positioning of certain elements on the first frame of the TitleScene. The footer panel appeared in the center of the screen; the text of the confirmation dialog buttons were not properly hidden (just the text, not the buttons); and the ship appeared in the center of the screen, even though it's supposed to be offset vertically.

There were several combined issues causing all of these problems, but it mostly just boiled down to my negligence when dealing with the order of system executions, or the slight delay before the first fixedUpdate() ran.

For the footer, I added a useFixedUpdate flag to the Attachment component so that the AttachmentSystem could optionally use the regular update() method to adjust attachments. This comes in very handy for positioning UI elements that aren't actually physically simulated. I had been adding PhysicalState components to them just so that I could use the AttachmentSystem, but that is no longer necessary. I updated the FooterSystem to adjust the Transform directly as well, rather than using the PhysicalState.

The text of the dialog buttons was a chicken-and-egg problem. The Text would get hidden by the ButtonSystem when the button's Drawable was hidden, and the Drawable would get hidden by the DialogSystem when the Dialog is disabled. If the ButtonSystem runs first, then the Drawable isn't hidden yet, so the text remains shown. If the DialogSystem runs first, then the ButtonSystem won't have created the button's Drawable or Text yet, so nothing gets hidden until the following frame. I added a simple visible flag to the Button component, and set it based on the enabled state of the Dialog in the DialogSystem. The ButtonSystem subsequently just shows/hides both the Drawable and Text based on the visible state of the Button.

The position of the ship is actually more of a problem with the position of the camera's PhysicalState, which gets adjusted by the CameraFollowSystem. Like I mentioned earlier, the first fixedUpdate() doesn't actually happen immediately, so there can be a visual delay (up to the fixed time step) before things appear where they are supposed to be. I decided to just make the first fixedUpdate() execute immediately for each new scene by setting the initial and reset() value of the TimeManager's _accumulator to the _fixedDeltaTime. That seemed to do the trick!

All of that was rough, but I'm stoked with the result - it's been bugging me for quite a while. All of the code changes can be found here, and the artifact-free version of the game can be played here!

While I was fixing this, I remembered that I never actually locked the app into a portrait orientation. I typically keep my phone's auto-rotation disabled, so I never had a problem with it rotating. When the thought occurred to me, I enabled auto-rotation, and tried to play the game in a landscape orientation. I was hilariously impossible. The ship is in the center as you might expect, but the obstacles come from the top of the screen so quickly that they are impossible to avoid (or intentionally hit)! The health bars are completely missing (since they are intentionally positioned quite a bit below the ship). An interesting experience, to say the least, but adding this snippet to the Info.plist will keep it from happening.

scampi/Info.plist snippet

<key>UISupportedInterfaceOrientations</key>
<array>
    <string>UIInterfaceOrientationPortrait</string>
</array>

21.3 Play Testing and Beta Publishing

My daughter had a soccer tournament this past weekend. For the first time, I passed my phone around and let everyone try my game - both kids and adults. The general reception was mixed, which wasn't a surprise. Some found it highly addictive, while others got bored within seconds. A lot of people commented on the difficulty, but not in a bad way - it seemed like they enjoyed the challenge, as compared to other games in the genre that don't require so much of your attention. Overall, I was incredibly happy with the performance and lack of game-breaking bugs - this kept the conversation about the game itself.

Here's a list of some of the feedback I received, along with some of my thoughts:

  • "I got 187 points, is that good?"
  • People are very accustomed to mobile games giving them a "rating" of their performance, often in the form of 1-3 stars.
  • Recording a high score could give the player a sense of relative performance, but as each "level" changes the dynamics of the game, a global high score probably isn't a great idea.
  • "I like that it doesn't have ads."
  • I didn't ask the question, but it's not entirely surprising to hear this from a kid who grew up in a world saturated with games littered with advertising. Wouldn't it be weird if the original Tetris had ads?
  • "You should make _" / "It would be cool if ___"
  • It's pretty common for players to want to contribute their ideas to a game, and it's a rare opportunity for someone to actually have the attention of the developer.
  • Some ideas included temporary invincibility, time-warps, and unlockables.
  • Specific ideas aside, this feedback makes me feel like the game isn't quite fleshed out enough.
  • "This is hard!"
  • I like that it's challenging enough to comment on, but not hard enough to make them quit.
  • "It's so retro"
  • Pay no mind to the fact that there are non-pixelated circles and triangle meshes in the game, kids seem to think it has an old-school vibe to it; relative to their age, I suppose they're not wrong.
  • I'll take it as a complement.
  • "I can't watch my shields and control the ship at the same time"
  • This one is actually a concerning user experience. It's really hard to position the shield health bars in a way that doesn't require the player "taking their eyes off the road".

Monetization Strategies

I've been thinking a lot about possible monetization strategies over the last month or so. I'm really not a fan of the current mobile game landscape. So many games are either packed full of ads or contain countless virtual items that you purchase with a collectable currency. It's not a terrible concept to have an in-game economy, but my problem with it is that it's impossible to collect enough of the currency to purchase all of the items - instead, the developers charge real money for buckets of fake money. Humans are suckers for immediate gratification, so when someone sees an item and realizes that they would either have to play the game for a month to purchase it, or simply spend $10 right now, I think a ton of those players would just cough up the cash.

Because of this type of in-game monetization, app developers have normalized the "free to play" concept. Most of these casual games charge nothing for the initial download, but they still come out ahead in the long-term if even a tiny fraction of the playerbase spends money within the game after the initial download.

This makes it really hard for a game like mine to compete. I can't simply charge a dollar for the game - why would anyone buy it when Subway Surfers or Temple Run can be downloaded for free? Am I just expected to receive no compensation for the effort I've put into building the game?

Using some realistic estimates, let's just say I've spent about 1,000 hours building the game. Furthermore, let's say a developer capable of building a game like this should make $50/hour. I should reasonably expect to make about $50,000 for the effort I've poured into the game, right?

Well, on one hand, software engineers generally make significantly more than $50/hour (in the U.S.). On the other hand, I very likely could have build this game in a much shorter amount of time had I used a prebuilt engine, or if I had any idea what I was building from the start.

If I somehow manage to make $50,000 off of this game, I'll call it a resounding success. Charging a dollar for the app means 50,000 people would have to install the game, which would require some serious marketing power. Furthermore, simply charging anything for that initial download severely limits the potential user base.

Another strategy I've seen is the "ad-supported" free version, in which the player can pay a dollar to remove ads from the game. This style keeps the large potential user base of a free app while still potentially making more revenue via "upgrades".

I still just hate intrusive advertising so much. What other options might I have?

In-game micro-transactions are incredibly popular. Often implemented as I described earlier, but sometimes players just pay for simple power-ups that help them beat their own high scores. Unfortunately I think the "pay-to-win" style is even more toxic than just shoving ads into their faces.

One last option occurred to me: Apple Arcade. Users pay a monthly subscription to Apple in order to access a library of games that contain no advertisements or in-game purchases. Users that don't pay for the subscription can still play the game via a one-time purchase. It kind of sounds too good to be true. Obviously I'd be limiting the potential user base by locking the app behind a purchase price or a subscription cost, but I think it's worth a shot.

Some further reading indicates that Apple might help with the development process, either with guidance or actual funding. It appears that they ultimately pay based on the amount of people that play the game, so I suppose the monetization would be similar to having ads in the game, albeit with a smaller user base. I wonder if they would assist in the marketing process as well?

Forming a Company

I should have done this a long time ago, but before I go down the path of releasing the app, I'd like to do so under my own limited liability company (LLC). I am not a lawyer, so I don't want to go into too much detail here, but there have been some things that I wasn't expecting.

Applying for an LLC in my state can take up to 7 business days. Fine. I've officially applied for the formation of Kravick LLC.

While I wait on that, I decided to look up additional requirements for my city. Yup, I need to apply for that too, though I'll have to wait for the official business certificate from the state.

I also looked up what additional requirements Apple might have. It turns out that they require a D-U-N-S number, which I will need to apply for once the LLC is certified, and can take up to 30 days!

Only then can I enroll in the Apple Developer program on behalf of my company. Once I can manage to establish that, then I can invite up to 10,000 users to test my app before the official release. Unfortunately, I can't actually start the process to apply for the Apple Arcade developer program until all of that is in place.

I guess actually publishing the app is on hold for now.

21.4 In the Meantime...

While I wait, there are still some things I need to do with the game. The feedback from the team was great, and it showed me that, while the game isn't a good fit for everyone, there is a viable target audience that enjoys playing it! This is the first time I've achieved that feeling on my own, and it feels great!

That said, the game definitely still needs work. Since I'll be waiting at least a month before I can even start a more public testing phase, I might as well do what I can to improve things.

Keep Your Eyes on the Road

I'm not surprised that people find it difficult to simultaneously focus on both the health of the shields and the oncoming obstacles. This type of situational awareness is what separates casual players from experts, and the problem exists in many other games. World of Warcraft has this exact same issue, and even allows players to customize their user interfaces in whatever way might suit them best. It's not uncommon for players to situate their party's health bars directly under the feet of their character, such that they can see everything in their peripheral vision simply by staring at the middle of the screen.

Our game is a bit different, however. While WoW has a much wider variety of possible situations, characters rarely move toward obstacles at 20 meters per second - and certainly not while they are expected to keep their party alive!

I've played around with different UI layouts in our game, and I think I've settled on the best of a bad situation. Keeping the health bars at the bottom of the screen often resulted in players losing track of their health, causing their eyes to bolt back and forth between newly spawned obstacles and the health bars. Stacking the health bars vertically along the edge of the screen seems to allow the player to see the bars in their peripheral vision no matter what else they are looking at on the screen.

One obvious issue with aligning the health bars along the edge of the screen is the reduced horizontal space on an already narrow screen. I experimented a bit with providing additional information on the opposite edge of the screen, in order to make the game more symmetrical, but doing so left the game itself with a tiny sliver of space down the middle of the screen. I got rid of the additional information (it proved to be more distracting than important anyhow), and settled on a layout that is offset a bit from the center of the screen, reminding me a bit of the old Game Boy version of Tetris.

Not only is it easier to keep track of the health of the shields, but it's also easier to tap the shields to heal them with one hand... if you're right-handed, of course.

An obviously ridiculous limitation, I went ahead and added a setting to the SaveManager to return the player's preferred Handedness. I added the option to the OptionsScene, whipped up a few conditionals to align things on the opposite edge of the screen instead, and spent an unreasonable amount of time asking myself questions like: "should I ask the player which hand they prefer?"

I decided that it would be unfair for a lefty to find themselves in a game that is incredibly difficult to play with no indication of how to make it easier. To top things off, they'd have to complete the tutorial and die just to get back to the options menu in order to switch the setting. So yes, when a new player selects the "new game" button, the player is first taken to a NewPlayerScene that asks them for their preferred handedness prior to starting the game.

While I was playing around with offsets and whatnot, I decided to nudge the ship and shields down the screen a bit, so that the player has more time to see things coming. The ship is always aligned with the middle of the stack of shields in order to keep everything in their peripheral vision as much as possible.

This commit contains all the code that went into it. I'll need to have some people play test the results before I'll be happy with it. You can play it here, though I'm sure it will be far in the future from writing this before you do, so I won't wait on your feedback.

Edge Layout

Power Ups

Our game currently has a single power up: a simple collectable mass heal that spawns roughly every 15 seconds. A fairly common feedback request is the addition of new power ups, so let's see what we can come up with.

  • Obligatory "god mode", similar to the star in the Mario games
  • Revive a depleted shield generator
  • Time warp, to help the player avoid obstacles at very high speeds
  • Autopilot, to help the player catch up on healing
  • Supernova! Clear the surrounding area of all obstacles
  • Basic speed boost, so the player can gather points faster if they are already healthy

At this stage in the development process, adding new features like this isn't just a matter of writing code. I'll need to whip up visuals and new sound effects as well, which could end up taking quite a bit of time. However, as I did with the early development of the game, I should first add a prototype of each feature to the game before wasting time on those polished elements, simply to make sure the feature is actually fun.

Ideally I'd like the power ups to spawn based on the current needs of the player, similar to Mario Kart - a player in first place often only gets defensive items, such as green shells, while a player in last place might get very powerful items like stars or chain chomps! In our game, a power up that revives a depleted shield is of no use if none of the player's shields are depleted yet! Slowing down time, on the other hand, might be more frustrating than fun to a player that is already going very slow.

Reviving Depleted Shields

I'm going to start by reviving shields because I feel like it's one of the easiest to implement.

I added a new Spell to the SpellDatabase which references a new Action named Revive. The new action simply chooses a random entity that contains the Health component, but does not contain the Alive component, and restores 25% of its maximum health, re-adding the Alive component in the process.

I added a new Revive value to the PowerUp::Type enum, and it was very easy to trigger the new spell in the ScoringSystem based on the type of the collected PowerUp. A little more complicated was the process of choosing which type of PowerUp to spawn inside of the SpawnSystem. I added a chunk of code that generates weights for each PowerUp based on the player's current situation. More specifically, MassHeal gets a weight of 10 for each shield that is not currently at its maximum health, and Revive gets a weight of 20 for each shield that is currently depleted.

That seemed to work just fine, but it made it a bit too easy for the player to recover from otherwise catastrophic events. I decided to add some code to respect the cooldown of each Spell. The SpawnPoint component now contains a std::unordered_map of the amount of time that has passed since the last time a given spell spawned. If the amount of time is shorter than the configured cooldown, then that particular PowerUp cannot spawn at all! I gave the Revive spell a cooldown of 60 seconds - since power ups spawn once every 15 seconds, you can only get a Revive power up at most once every 4 power ups. This feels way better, and allows the player to recover from minor mistakes without trivializing the recovery process!

This concept of a "hidden" cooldown is referred to as an "internal" cooldown in World of Warcraft. Internal cooldowns are generally easy to discover in the game data using external tools, but most players don't think too hard about them.

In the case of our game, if the player decides to forego collecting the power up for some reason (or misses it accidentally), then they are still locked out of it for the entire cooldown period.

Speed Boost

Initially I implemented this by simply adding a constant value to the Player's speed. Since I can't add huge amounts of permanent speed without quickly overwhelming the player, the power up either didn't feel very impactful, or felt more detrimental than helpful. I decided instead to create a new Boost component, containing a magnitude and a duration. When the player collects a speed boost, they immediately get a large burst of speed, but it fades away over the configured duration, eventually settling at the speed they would have been at had they never collected the power up at all. Due to the transient nature of the speed boost, the power up can grant large amounts of speed without feeling super overwhelming.

To really drive home the impact of the speed boost, I also added a little bit of screen shake, and configured the ship's engines to emit orange "flames" while the boost is active.

With the ship moving so fast, it tends to collide with a lot of obstacles in very quick succession, making the current additive screen shaking a bit too much. I decided to refactor how the ShakeSystem works - instead of the camera containing a single Shake component and all the sporadic systems adding to the current magnitude and duration, the systems will create separate entities (each with their own Shake component). The CameraFixture now has a shake flag to enable the feature. The total magnitude of the shake will still be additive, but this allows each Shake to correctly fall off in duration, rather than indefinitely extending it. The result is much less intense, but a lot more tolerable.

The addition of this power up alone provides a ton of depth to the game play. Good players capable of keeping their shields healthy are rewarded by speeding up the run a bit (but not too much), while struggling players are assisted with a healing power instead. If the struggling player were to have received a speed boost, the game would be even more punishing!

I decided to change how the MassHeal works. It's always healed for a flat amount, but as the shields gain health, the power up feels less effective. I changed the logic to heal each shield for 25% of its maximum health instead of the flat amount. It's still very impactful at lower levels, but since it's equally impactful at later levels, and it only spawns when the player actually needs it, the player isn't tempted to skip it altogether.

Autopilot

Writing an AI capable of navigating through the obstacles isn't impossible, but I'm going to hold off on implementing this for now. With the addition of the the speed boost and revival power ups, along with the increased power of the heal power up, I no longer thing it's necessary for the game to play itself.

I actually have some other ideas for how to restructure the game's progression, but I'm not ready to write those down yet. Let's keep implementing new power ups.

Time Warp

I kind of like the concept of a power up that temporarily slows the flow of time, allowing the player to more easily navigate through obstacles while catching up on healing their shields. However, I think there's one big question that needs to be answered: would the player be upset if they received a time warp instead of a healing power? In theory, the time warp power up should only appear while the player is already traveling at very fast speeds and their shields are not healthy - but at what speed would you be willing to sacrifice such a large amount of "free" healing?

The best way I've managed to balance it is to make the time warp effect have an equal weight as the healing power, but only when the player is already moving quickly. I also noticed that the time warp effect becomes slightly annoying if it lasts too long, so I shorted its duration down to 2 seconds. Well, the player would experience 2 seconds, but the engine thinks it's just 1 second, since time has been slowed by 50%.

While testing the feature, I noticed that the game looked a bit "janky" while time was slowed. This immediately made me think that there was something wrong with the PhysicsInterpolationSystem, or at least some problem with the order of operations. I commented out most of the entities in the TitleScene and set the smoke emitter to only emit one particle at a time. I added a few debug logs into the PhysicsInterpolationSystem to see what was going on, which didn't help, since I could already see what was happening. I also increased the fixed time step in the TimeManager to twice-per-second so that I could really see the results of the interpolation. More coincidentally than intentionally, I happened to re-order some of the systems in the scene (namely the PhysicsInterpolationSystem and VelocitySystem) and once more, everything worked as expected. I'm glad I got it working, but I decided to do a little bit of cleanup in the PhysicsInterpolationSystem while I was in there. I also needed to make some updates to the AttachmentSystem and CameraFollowSystem to prevent the visual interpolation, since the whole point of those systems is to track some other entity. I reverted the change to the fixed time step, and the time warp power is as smooth as butter!

I'm not convinced this power up is all that great, at least not in the current iteration of the game (something's cooking in my head). I'll leave it in for now, maybe it will come in handy later. At least it helped me resolve a fairly large bug! The game has effectively only been running at 50 FPS, even though it's rendering way more than that. The worst of both worlds.

Invulnerability and Supernova

The other ideas are really cool concepts, but I'm going to set them aside for now in order to focus on the restructure that I was hinting at before. I haven't added any new mesh types or sound effects for the new power ups, but you can take a look at all the crazy changes and fixes I've made here, and the playable version can be found here.

21.5 Restructuring Game Progression

My wife and daughter have been having a lot of fun trying to beat each other's high scores ever since I added the new power ups to the game. I love watching their faces as they play, with their emotions ranging from stoic confidence to utter panic. During the phases of panic, they have the biggest grins on their faces - they get a thrill out of it. My favorite part is during the most chaotic scenarios, they still feel a glimmer of hope that they can recover from the situation and make it a just little bit further.

Unfortunately the game is currently structured such that they can never really "win". Sure, they can beat each other's high scores, but the game always ends in the player's death, and that kind of sucks. One of the most rewarding scenarios in a game like WoW is when most of the group has died, but a couple of completely disadvantaged players, against all odds, still manage to defeat the boss. That feeling doesn't currently exist in our game.

I have observed that my family only really likes to play the game once I've already progressed through the early game. No one seems particularly interested in the lower levels with one or two shields, where everything is very slow. The make matters worse, the player doesn't actually unlock more shields or speed until they die. The first level is so easy that a new player could conceivably gather hundreds of points (very slowly), get bored, and quit the game, before they even unlock the upgrades as a result of gathering those points.

So I've been thinking a lot about what a winning condition might look like in our game, and how the game's progression should fit into that model. Some ideas have been more interesting than others, ranging from plain old score-chasing to a fully fleshed out racing experience like F-Zero. The model that I think would work best for a casual experience is the same system I've described before: a series of levels with well-defined completion points (distance), with the player's performance (points, number of shields remaining) being rated between 0 and 3 "stars".

However, I don't want to neglect the enjoyment that my family has obviously gotten from chasing each other's high scores, so I also want to offer an endless/endurance mode, perhaps made available once the player completes the rest of the game.

This restructure is not a trivial amount of work. It entirely uproots the previous progression system, hopefully in a way that makes more sense to the player. I'm thinking that the levels can be organized into "worlds", where each world introduces a new mechanic, and each level within that world helps the player master that mechanic, culminating in a final level that really tests the player's mastery of all of the existing mechanics introduced thus far.

Some of the levels might contain some scripted events rather than completely randomly generated elements. I don't have any specific examples in mind, but I could see a situation where I'd want to carefully place multiple objects in the level in order to emphasize an interaction between them.

Time Wasted

I spent the last several days refactoring the game, utterly ripping some things apart, and organizing things into reusable chunks to streamline the level creation process. I'm definitely making too many changes in a single commit here, but I wanted to iterate as quickly as possible. I managed to finish up most of the levels, but the game itself is only playable from levels 1-11 because level 12 doesn't actually point to anything (as you can see in the new LevelDatabase).

There's a new LevelSelectionScene that just displays 20 level selection buttons on the screen, disabling the ones that the player hasn't unlocked yet. It also displays circles representing the number of "stars" that the player has managed to achieve for each level - the only way to get 3 stars is to collect all of the asteroids. In the early levels, it's pretty trivial, but it gets really tough as the player has to weigh the pros and cons of collecting speed boosts.

I also created a LevelCompletedScene, which is the "success" counterpart of the GameOverScene. The new scene makes it more obvious when the player has unlocked an upgrade, after displaying the number of stars the player achieved during the run.

The bulk of the changes are modifications to the SpawnSystem to allow spawn rates of elements to be more configurable. Most levels simply use a new RunnerScene, which is a configurable version of the InfiniteRunnerScene, except it also requires a distance that the player has to travel before the level is completed. If the player can survive to the finish line (also spawned by the SpawnSystem), then the level completed (as handled by the new CompletionSystem). Some levels extend the RunnerScene to configure specific scenarios - the LevelNineScene randomizes the health of the shields at the start in order to emphasize the value of the regen power up.

The entire process was guided by a spreadsheet that I whipped up a few days ago, detailing what each level should contain, describing its general purpose, and declaring how long the it should be.

The game is definitely structured more like a traditional mobile game, but I kind of hate it - and I'm not alone! Everyone I've let play the game has made similar comments.

  • The game is too slow for too long
  • The levels aren't different enough
  • This level is too easy

In my attempt to create a logical progression system, I unintentionally stripped the game of what made it fun. The fun part of the game was that it got faster and faster until the player got completely overwhelmed. Splitting the game into "levels" with "finish lines" effectively prevents skilled players from ever getting to the fun part, and bad players will be overwhelmed before they even make it to the end! Whenever a player manages to finish a level, they just have to start the next one at their painfully slow base speed.

You can play this iteration of the game here, but I wouldn't recommend it.

Iteration

I've been thinking about what exactly I didn't like about the previous iteration of the game. A point-based leveling system wasn't necessarily bad, it just felt weird to only level up once you finally died, sometimes jumping several levels at a time. What if the player could gain their upgrades as soon as they earn them?

I whipped up a little proof-of-concept of this idea by hard-coding a big conditional statement to check if the player had achieved 25/75/150/250 points, and grant them one additional shield whenever that condition is met. I wired the InfiniteRunnerScene back up to the TitleScene and NewPlayerScene and played the game from the beginning, and I really like it.

My brain is going in a lot of different directions right now, so let's see if I can manage to sufficiently articulate a single train of thought.

Should the player have permanent progression at all? If they die, should they start back from the very beginning? Well, the very beginning is very slow, since it's intended for new players, so I don't think that's the right move, however...

What if we could always put the player under constant pressure of some sort of permanent loss? Perhaps we can introduce the concept of "lives"/"credits" into the game, similar to old arcade games (this is a "retro" game after all). The player could keep retrying, using their unlocked upgrades, until they run out of lives, in which case they have to start over from the very beginning.

How should the player receive more lives? Rare collectable power ups, perhaps. Maybe they can choose to sacrifice a chunk of their current points for another life. Is sacrificing a chunk of points really that big a deal if they are already at the max level? Maybe it would be a bigger deal if they were competing for a top spot on a leaderboard!

Once again, I spent several days making a ton of code changes in a single oversized commit. I already deleted a lot of the systems and scenes that I wrote in the previous commit, and even reinstated the LevelTrackingSystem that I had previously deleted (I love being able to reference historical code in source control systems).

Most of the new code is simply reorganizing the visual elements of the game. The InfiniteRunnerScene and GameOverScene now have a row of information at the top that includes the player's current level, total points, and number of lives. The information is attached to a central Header component, which is basically a duplicate of the pre-existing Footer component, except they are now both handled by an EdgeSystem, which is just the FooterSystem with a new name. Perhaps most interesting is that this system is aware of any "inset" padding that the platform requests. You see, my iPhone has a notch at the top of the screen which contains a camera and other sensors, so it wasn't appropriate to align all of that information at the very top of the viewport. Instead, I added some getters and setters to the Viewport class so that the platform code can set any relevant information. In Scampi's case, we can use UIApplication.sharedApplication.windows.firstObject.safeAreaInsets to set the insets of the Viewport in the ScampiViewController.

The biggest functional change is the introduction of the UpgradeSystem (didn't we have an UpgradeSystem that did something completely different in the past?). This new system is responsible for detecting when the player gains a level and apply any relevant upgrades, without requiring the player to die and start over!

The problem with shields being dynamically added is that the player might go to heal a shield the same moment a new one appears and moves everything around. The experience is a bit jarring, so I modified the HudSystem to effectively "lock" each shield in a fixed position. New shields appear around existing shields, but they never move.

Introducing "lives" into the game was just a matter of modifying the SaveManager and requiring the player to start a new game in the TitleScene when their lives reach zero. The GameOver scene also foregoes its usual "level up" animations when the player's run is over. I decided to award the player 1 additional life per 1,000 points that they receive. It seems simple enough, but I might revisit it later.

I also randomized the stars in the background a little bit more, and I think they look better, though no one has ever complained about them before. My daughter thought the game started off way too slow, and even mentioned that Subway Surfers doesn't even start very slow, so I increased the base speed of the player to 5.0f, up from 3.0f. I also made some additional fixes to the inconsistent header positioning here by manipulating the Attachment offsets rather than the Transform directly (which would then get adjusted by the AttachmentSystem).

Well, I just noticed that the life indicator icon disappears when you die. Since I copied the entire ship composition (and just set everything to white), all the pieces of the tiny ship icon also had the ShipPart component on them. When the player dies, the LivenessSystem destroys all entities containing that component, resulting in the icon disappearing. That has been fixed here.

I'm pretty happy with the way things are looking right now. I need to sleep on it a bit to know for sure if I'll change my mind, but you can check it out here.

Continuous Leveling

21.6 Cleanup and Polish

I'm relatively happy with the state of the game right now, but there have been a few little things bothering me, so I wanted to take some time to fix them up.

Semi-Transparency

First of all, the header indicators tend to blend in with the actual game objects behind them. I want to add a semi-transparent black background behind each indicator, but our ColoredFeatureRenderer doesn't currently support configuring the alpha channel. There's no real difficulty here, I just updated the ColoredFeature's color value to be a glm::vec4, which broke a ton of pre-existing code. I fixed most of it by updating the Palette, which was awesome. However, there were a ton of old unused systems and components that needed to be updated that I just didn't want to deal with, so I went through and deleted every unused piece of code that has rotted over the last several months.

Updating the actual feature renderers to handle the newly defined alpha channel was trivial, but I had to do quite a bit of math to correctly align the new background entities to their correct positions behind their respective indicators. I stuck all that math in the same systems that handle updating the values of the indicators: the HudSystem for lives, and the ScoringSystem for the score and the current level. I adopted a pattern of each indicator's component containing the ID of the icon next to it, and the background behind it, so that the systems can just reference those entities by ID rather than querying for them. This pattern meant I was able to delete the ScoreIcon component entirely. I also noticed that the Friendly component in the InfiniteRunnerScene isn't actually queried by anything anymore, so I deleted it too.

Hiding the System Status Bar

On iOS, the system's status bar (containing the clock, WiFi/cellular signal indicator, and battery) has been displaying at the top of the game this whole time. I've debated with myself whether I wanted to get rid of it or not, but having my own indicators at the top of the screen has pushed me into a firm position of getting rid of the system's status bar. A quick Google search led me toward a solution requiring updates to both the Info.plist and the ScampiViewController.

scampi/Info.plist snippet

<key>UIStatusBarHidden</key>
<true/>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>

scampi/src/uikit/ScampiViewController.mm snippet

- (BOOL)prefersStatusBarHidden {
  return YES;
}

Well that was easy.

Spell Queueing

I've also wanted to experiment with spell queueing, which would allow the player to select the shield they want to heal next, even before the cooldown has completed. I implemented this in the PlayerControllerSystem using an orange selection indicator, which appears as a frame around the shield that has been Enqueued. Once the cooldown is ready, the highlighted shield is automatically healed and the indicator disappeared. The game is definitely easier to play with this in place, so I'll leave it, but I'm not entirely sold on it as the final solution.

Cooldowns During Time Warps

The time warp power up has been an interesting addition to the game, but it also slows down the recovery rate of the player's heal cooldown. I updated the CooldownProgressSystem to divide the delta time by each TimeWarp's factor. The end result is that cooldowns recover at a constant rate, no matter how slow or fast time is going in the game! It definitely feels more natural, for some reason.

Toast Alignment

Apparently the math in the ToastSystem assumed the scale of each Toast entity's Transform was 6.0f. I fixed that, so now the level and upgrade Toasts are properly centered above the player.

Power Up Meshes and Effects

The recently-added power ups have been lacking visuals since I added them, utilizing simple quads as placeholders instead. Unsurprisingly, players aren't sure what to expect when they collide with the squares. It's already been a few weeks since I implemented them into the game, so I guess it's time I promoted them to first-class elements.

For the speed boost, I created a SpeedBoostMesh that is made up of 2 chevrons, pointing upwards, each containing 3 triangles. This pattern is used in many games, and players should immediately recognize its purpose when they see it. The quad was already yellow, so I left it like that - it just seems to fit. I already added some screen shake and "fire" effects to the player's ship when they collect the speed boost, so I think I'll leave it just the way it is for now.

The time warp is a little more difficult to convey to the player. I thought about showing a clock, and my wife suggested a swirly pattern. I decided against those, not because they aren't good ideas, but because they would be hard to build into a mesh without some sort of image sampling. Instead, I created a new TimeWarpMesh that is made up of 10 triangles in the form of an hourglass. One of the complaints I've gotten from players is that, even once they figured out what the light-blue power up was doing, they had trouble determining how long it would last. I decided to add a particle effect around the player, consisting of 12 circles (like the numbers on a clock). The particles follow the player, while collapsing inward onto the player over time, using a new CollapseSystem, which just modifies the offset of the associated Attachment. The duration of the new Collapse component is tuned to align with the duration of the TimeWarp. This should give the player a sense of how long they have until the effect expires. I left the power up the same cyan color that it was previously, and set the particles to match.

Finally, I implemented a new mesh for the revive power up. I really wasn't sure what to implement here, since the shields aren't living creatures that are literally being revived. I bounced between ideas like a lightning bolt, a battery, or a literal shield icon. I decided to just use a lightning bolt, implemented using 3 triangles in a new ReviveMesh. When the player collects the power up, 10 yellow particles appear at the location of the collision with a quick but intense screen shake, and then rapidly dissipate. The revive was previously blue, but it was very hard to see against the navy background. Since it's now in the shape of a lightning bolt, I made it yellow instead.

Because I used an enum instead of an enum class for the MeshType, I was getting compiler errors due to the reused values TimeWarp, Revive, and SpeedBoost. I converted the enum into and enum class and updated all the relevant touch points.

I had been contemplating the possibility of making the game elements use more than one color - for example, the mines could have a pure red outline with a darker red interior. I actually implemented this into the game using another entity, attached to the primary entity, with a smaller scale. I was not pleased with the result. No matter how much I fiddled with the different shades of red, I could not get it to look good. The closest thing I got to a "cool" look was a pure black interior, but it didn't fit in with the rest of the game at all. I might use the concept for another idea in the future, but for now I'll just undo these changes and move forward. In my experimentation, I added more basic colors to the Palette, and I kind of like it like that, so I'll leave it.

Tuning

This game is fun. It's not the best game in the world, but I'm incredibly proud of what I've managed to accomplish, and I can't wait to get it in front of some new faces. Until then, it's only reasonable that I try to make the game as enjoyable as possible. I have a couple of ideas I'd like to play around with.

The player's speed is the single most impactful knob we can turn in order to adjust the difficulty of the game. The game is entirely trivial at low speeds, and seemingly impossible at very fast speeds. Even if the player can manage to continue avoiding all of the mines, the rapid damage intake from asteroid collisions can add up quickly.

I recently mentioned that the game starts off very slowly, to the point that I increased the initial speed of the player during a new game. I don't think that was necessarily a bad idea, but I think there could be a better system in place here.

Some recent feedback I got from a friend was something along the lines of: "Why would I want more acceleration? I'm already going too fast!" - I laughed and explained that the game is supposed to get harder, but he kind of has a point. The upgrades should probably help the player, rather than make things even harder, but I like the increasing difficulty of the game.

I've been playing around with the idea of removing passive acceleration from the player. Instead, each asteroid grants the player a small speed boost, relative to the size of the asteroid. I wired up the asteroid-based acceleration to the acceleration upgrade rank. This kind of gives a logical reason as to why we are colliding with the asteroids: we're using them for fuel!

Mines, conversely, reduce the player's speed by a percentage. This isn't the first time I've toyed with this idea. This gives the player an opportunity to really control their speed, if they so choose, albeit at a very high risk.

In an attempt to keep the player's speed under control, I decided to change the "base speed" upgrade into a "max speed" upgrade. The base speed upgrade doesn't feel good in the current iteration of the game simply because the player doesn't get to utilize it until they die. Unlocking a base speed upgrade on your last life is kind of a slap in the face.

Being able to upgrade your maximum speed effectively caps how difficult the game can be at any given level. The skilled players who reach the higher levels will be met with a higher level of difficulty, which is exactly what we want!

With no more concept of a "base" speed, I had to decide how I wanted to handle the starting speed for the player. I decided to just start at 50% of their currently unlocked maximum speed, so that the game continues to start at 5.0f m/s (since the baseline maximum speed is 10.0f m/s). Each upgrade will grant the player an additional 2.5f m/s of maximum speed, which means their base speed will increase by 1.25f.

While giving the player control over their speed is more engaging, a skilled player can keep themselves alive indefinitely, leading to very long play sessions. While testing this iteration, I spent well over half and hour playing the game before finally running out of lives. What's more interesting is that I was often gaining at least 1,000 points per life, receiving an extra life, making my future death net-neutral.

Long play sessions are not very friendly to casual players. Having spare lives makes the game more forgiving of one-off mistakes, but makes it take much longer before finally reaching your final score. If the goal of this project is to introduce a leaderboard, then perhaps these long play sessions aren't a great idea. I think I'll start by removing the free lives, and drop the starting lives down to 3.

That was definitely a step in the right direction, but it still takes a really long time to progress through the levels. I'm going to adjust the LevelCurve to really shorten and flatten out the amount of time it takes to reach the next level. I'll also decrease the spawn rate of mines from 65% to 45%, and increase the chance of something spawning from 85% to 90%.

New XP Curve

The result is that it takes about one-third of the time to get to the max level. Early levels have a slight ramp in XP requirements, which then tapers off into a pure linear function after level 10. Even after these changes, it still takes me about 9 minutes to reach level 20! That is not very casual.

Long play sessions aside, I've noticed that the new speed-related mechanics (asteroids speed you up, mines slow you down) are the core of the indefinite survivability. When I collide with a mine, I lose a chunk of speed, but I can maintain the slower speed as long as I don't collect any asteroids, allowing me to heal myself up before gaining any more speed.

This level of survivability is at odds with the entire concept of multiple "lives". Lives were introduced as a way to help players reach the end game, allowing them to take more risks early on, or play cautiously once they know their run is about to end. Lives were also added in response to the game having no maximum speed. With a level-based speed limit, it's much more likely for the player to reach the higher levels before experiencing any trouble. Because of this, I'm just going to give the player one life to really prove themselves.

These changes are all over the place, but it's really nothing too complicated. You should definitely give it a shot.

21.7 Beta Publishing, Revisited

Over the last several weeks, as I've been continuing to iterate on the game's mechanics, I've also been taking baby steps toward establishing an Apple Developer account under my new company name. Every single step of the process seemed to take a few days, and as each step was completed, I would move forward with the next.

Kravick LLC has been officially established, and so has the Apple Developer account under its name!

Today, I spent entirely too much time figuring out how to publish a test build of the game to a list of beta testers. While the app hasn't yet made it past Apple's required "review" process, I was still able to verify and upload the latest build of the game, albeit after a lot of headaches.

Apple really wants iOS developers to use Xcode. It took me practically the entire day of directionless Google searches and trial-and-error code changes before I finally figured out which pieces needed to fall into place.

This entire time, I've been building and deploying a .app bundle of the game to my phone without any issues. Actually uploading the app isn't so simple, however.

First of all, the app has to be compiled with the correct code-signing certificates. These certificates are automatically managed by Xcode, though you can painstakingly generate them and download them from the Apple Developer portal instead.

After that, the app has to be built into an .xcarchive archive. This archive seemingly just contains the signed .app bundle with a tiny bit of additional metadata. There's no documentation available on how to manually structure this type of archive, but I did manage to find a command using xcrun which was supposed to be able to generate such an archive from and existing bundle. Unfortunately the command was removed in recent versions of Xcode (go figure).

I found a replacement command that used the xcodebuild command instead. While the command seemed promising, the output archive was always empty. It took me hours to figure out the secret sauce of setting the SKIP_INSTALL Xcode property to No. After that, the output archive correctly contained the bundle.

Only then could I convert the archive into an .ipa file. This format is basically just a ZIP file with a different file extension. While I could create it manually very easily, I decided to just use the associated xcodebuild command to do it for the sake of consistency. Unfortunately that command required its own XML file containing "options" for the package. I spent another annoying chunk of time figuring out how to configure that.

With a compiled .ipa file finally available, I tried to use Xcode's altool utility to verify the file. Naturally, the verification did not go smoothly. Most notably, it complained about my Info.plist's app icon settings. No matter how many different ways I tried to modify that file, the verification kept failing. After a lot of Googling, I broke down and decided to use Xcode's "asset catalog" mechanism to bundle my icons into its own file format. Wouldn't you know it, the validation passed just fine.

I really hate being forced to rely so heavily on Xcode. It's one thing to require using the toolchain. It's a bit much to require custom commands that I can shim into CMake. It is entirely unacceptable to require me to actually open up Xcode and use its UI to generate a proprietary file for no obvious reason. My app icon worked just fine on my phone without the asset catalog! Why does the App Store seem to require it?

In any case, once I was finally able to verify the app, actually uploading it (again, using the altool utility) was painless. I was able to add myself to an internal testing group and push the build to my phone using Apple's TestFlight. I created an external testing group and added a bunch of my friends who have been waiting patiently for actual iOS builds, but unfortunately the app must go through some sort of review process before I'm allowed to push it to external users.

I guess that's one more step that I'll have to wait on.

Once that happens, then I can generate a public link to hand out to potential players. Perhaps more importantly, I can use it for the Apple Arcade application.

You can view the relatively small amount of change that I made to enable distribution of the app from within CLion. I've created a new CMake profile in my IDE that contains the relevant environment variables for building a distribution archive, and the CMakeLists.txt adds a couple of new targets that I can invoke to perform the steps. It's not the cleanest thing in the world, and I'm 100% positive that Apple has made the entire process completely seamless for Xcode users, but I'm just glad to have figured it out.