So far in this series I’ve covered how to build a largely static screen in SpriteKit. Today I’m going to cover 2 things
- I’m going to move our dude from left to right under force
- I’m going to scroll the camera with him
The entirety of the code changed in this post is here.
Moving the player
So rather than build a Mario type game, I want this to be more like a continuos runner in the vein of Canabalt (for now at least), so to simulate our movement I’m going to apply a constant pressure to the player every frame. This is where we hit our 1st major problem in the current game code.
The problem is that so far I’ve use async workflows to give a nice linear look at game flow, and SpriteKit does enough ‘magic’ that it hangs together and it all works. But to apply force to the player, doing it once isn’t enough – I need to do it constantly – preferably every frame.
One approach is to subclass SKScene and move all our game into the Scene. I don’t like this for a number of reasons. The chief one is that it mean we have game logic code outside of our level async workflow, defeating the point of having it, and reducing us back to the classical OO game state management.
My approach is to subclass SKScene, but expose an event for when the Update method is called. Now using this we can subscribe to the event in our workflow, keep our logic local, and because of the use binding have it cleanup when exiting the async block! Here’s the code:
type Scene(size:SizeF) = inherit SKScene(size) member val UpdateEvent = Event<_>() override s.Update time = s.UpdateEvent.Trigger time
And later in the level code:
//Add event to know when Update is called use _ = scene.UpdateEvent.Publish.Subscribe(fun time -> player.PhysicsBody.ApplyImpulse(CGVector(2.0f,0.0f)))
Scrolling the view
One of the areas I think a lot of sprite based APIs gets it wrong is by making scrolling about moving the sprites, whereas really you want to be moving the camera. Unfortunately SpriteKit makes this mistake and is about moving Sprites. This is a shame because it looks like it should work; An SKScene has a Position property, just like every other SKNode. Great! But if we set that we get no runtime behaviour other than a warning in the debug output:
2013-10-14 05:46:25.836 PlatformGame[13852:a0b] SKScene: Setting the position of a SKScene has no effect.
This is one of the ways that SpriteKit clearly misuses OO and ends up with a slightly wonky inheritance structure. No biggie, but it does mean that we cannot scroll by moving the scene.
Luckily for us SpriteKit helps us overcome this. I’m still going to take advantage of the Position of an SKNode, but I’m going to add in a “ScrollNode” (which is just an SKNode) an in the level workflow I’m going to take everything I added to the SKScene and add it to my Scroll Node instead. I’ll then move the scroll node.
use scrollNode = new SKNode() scene.AddChild scrollNode
Now we can handle the scrolling the same way we handled the player updating, except we cant do it from the Update event. When we update the player physics nothing is applied until later, but SpriteKit offers another method in SKScene to override called DidSimulatePhysics(). If we follow the same pattern as above with the Update event we can fill in our new Scene so that it looks like this:
type Scene(size:SizeF) = inherit SKScene(size) member val UpdateEvent = Event<_>() member val DidSimulatePhysicsEvent = Event<_>() override s.Update time = s.UpdateEvent.Trigger time override s.DidSimulatePhysics() = s.DidSimulatePhysicsEvent.Trigger None
And once again we’ll consume this in the level async workflow:
//Move the scroll node inversely to the players position so the player stays centered on screen use _ = scene.DidSimulatePhysicsEvent.Publish.Subscribe(fun _ -> scrollNode.Position <- PointF(320.f - player.Position.X,0.0f))
But regardless of whether you have heard of it or not, Parallax scrolling is a cool effect. For those who havent heard of it, its the effect of moving something in the background more slowly than something in the foreground, creating a pseudo 3D effect.
So to do this in SpriteKit is similar to the scrolling method – first we add a scrollNode for the parallax layer, then we add some stuff to it (but this time with no physics – its purely background), and when we offset the scroll we do it at a fraction of the full speed.
Here’s the code broken into bits.
use parallaxScrollNode = new SKNode() scene.AddChild parallaxScrollNode //Pop in a parallax layer for i in 0..50 do use hill = new SKSpriteNode "hill_small" hill.Position <- PointF(float32 i * 70.f, 50.f) hill.ZPosition <- -1.f parallaxScrollNode.AddChild hill
Note we set up the hill with a Z position to move the layer “back” into the scene. Then in the DidSimulatePhysics event we will offset the parallaxScrollNode –
//Move the scroll node inversely to the players position so the player stays centered on screen use _ = scene.DidSimulatePhysicsEvent.Publish.Subscribe(fun _ -> scrollNode.Position <- PointF(320.f - player.Position.X,0.0f) parallaxScrollNode.Position <- PointF(-player.Position.X / 2.f, 0.f))
Today I’ve taken 2 closely linked ideas (player movement and scrolling) and tackled them in SpriteKit. We’ve had to introduce some inheritance and OO code to get the results we need, but this is one of the key strengths of F# – the ability to mix and match OO and functional programming in a no-compromise solution. The code, as always is at https://github.com/neildanson/PlatformGame.
Obligatory screenshot (if you don’t have Xamarin iOS you’ll just have to trust me that this is scrolling!)
Next time, I’m going to tackle sprite animation and add some sound to the game.