Spritekit Automapping Adventure

I have decided to develop a few games in 2022. In an effort to update my skills I’ve set about writing a Spritekit game with SwiftUI. In my head I would like to make a fairly large, 2D action-roguelike game where your character has to traverse an infinite number of “levels”, ward off the baddies and manuever whatever the story is. It’s a pickle!

Best place to start is the beginning so I created a new iOS app. I always liked to use a Navigation Controller to handle the main menu type stuff and starting with this in mind realized I had no idea how to create a navigation view in SwiftUI. Apple’s Docs are fairly clear on implementation, but the simplicity of things within SwiftUI initially through me for a loop. The ability to create everything required for navigation, e.g. stack, controller and subsequent views, for a Main Menu system is around 11 lines of code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct MainView: View {
var body: some View {
NavigationView {
VStack{
Spacer()
NavigationLink(destination: game) {
Text("New Game")}
NavigationLink(destination: settings){Text("Settings")}
Spacer()
}.navigationBarTitle("")
.navigationBarHidden(true)
}
}
}

There is a lot going on in those thirteen lines and it looks dramatically different than the bracketed world of Obj-C that I am more intimiately familiar with. I’m not going to dive too deep into those differences in this post, but will write something up in the future. Off of my menu there are two Views game & settings. Settings ATM is just a blank view but Game is the bread and butter. Inside I create a SKScene object to render and house game components, e.g sprites, tiles, player node, joystick, and then utilize SpriteView to draw it within a SwiftUI View.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct SpriteViewWrapper: View {

let width = 256.0
let height = 256.0
var scene: GameScene {
let scene = GameScene() // This is our SKScene
scene.size = CGSize(width: gameFrame.size.width,
height: gameFrame.size.height)
scene.scaleMode = .fill
scene.viewID = index
return scene
}

@State var index: Int
@State var gameFrame: CGRect

var body: some View {

SpriteView(scene: scene)
.frame(width: gameFrame.size.width,
height: gameFrame.size.height)
.ignoresSafeArea()

}
}

This loads our GameScene():SKScene object and now we finally render whatever is contained within. Voila!

For the tiles I’m leveraging GamePlayKit to generate uniformed noise and then creating physics bodies for each tile to handle collisions. A player, currently playerNode = SKShapeNode(circleOfRadius: playerRadius), is added atop the map tiles and a Camera SKNode is generated with the joystick and other HUD elements attached to it to allow for everything to smoothly move on scroll. Scroll is accomplished by grabbing the velocity of the touches for the Joystick object and on the GameLoop update applying a scale of this velocity to the Player, which updates it’s x & y position, and then set the Camera’s current position to the Player’s updated. Keanu whoa

The only tricky bit in most of this is that Spritekit doesn’t allow one to simply set a collisions on an entire TileGroup, i.e. the rock or water tiles, so they are attached in the same loop that the tiles’ render is accomplished. The last bit of code accomplishes this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func createCollisionNode(col: Int, row: Int, tileSize: CGSize, halfWidth: CGFloat, halfHeight:CGFloat, collisionBitMask: Int, categoryBitMask: Int) -> SKShapeNode
{
let x = CGFloat(col) * tileSize.width - halfWidth
let y = CGFloat(row) * tileSize.height - halfHeight
let rect = CGRect(x: 0, y: 0, width: tileSize.width, height: tileSize.height)
let tileNode = SKShapeNode(rect: rect)
tileNode.position = CGPoint(x: x, y: y)
tileNode.physicsBody = SKPhysicsBody.init(rectangleOf: tileSize, center: CGPoint(x: tileSize.width / 2.0, y: tileSize.height / 2.0))
tileNode.physicsBody?.isDynamic = false
tileNode.physicsBody?.collisionBitMask = UInt32(collisionBitMask) // Match Player's Bit Mask
tileNode.physicsBody?.categoryBitMask = UInt32(collisionBitMask) // Match Player's Bit Mask

return tileNode
}

It’s off to a pretty good start. Adding health, enemies and eventually some core hacky-slashy mechanic will be next on the list!