SpriteKit is a powerful framework provided by Apple for creating 2D games and interactive animations. It's a versatile tool for game developers, allowing them to bring their creative ideas to life. In this article, we'll explore how to create the classic "Snake" game using SpriteKit. "Snake" is a timeless arcade game that involves controlling a snake to eat food and grow longer while avoiding collisions with the walls.
Before we dive into the development process, make sure you have the following set up:
Let's get started by creating a new SpriteKit project in Xcode:
To begin with, we will determine the components of our game, and we will make the masks for collisions for them
enum CollisionMask: UInt32 {
case snakeHead = 1
case snakeBody = 2
case apple = 4
case edgeBody = 8
}
In the update(_:)
method our game scene, you'll need to implement collision detection to check if the snake has collided with the food or edge. When the snake eats the food, it should grow longer, and a new food item should appear.
To create the snake and food objects, we'll use SKShapeNode
.
final class Apple: SKShapeNode {
convenience init(position: CGPoint) {
self.init()
self.path = CGPath(ellipseIn: CGRect(x: 0, y: 0, width: 10, height: 10), transform: nil)
self.fillColor = .systemRed
self.strokeColor = .systemOrange
self.lineWidth = 2
self.position = position
let pBody = SKPhysicsBody(circleOfRadius: 10, center: CGPoint(x: 5, y: 5))
pBody.categoryBitMask = CollisionMask.apple.rawValue
self.physicsBody = pBody
}
}
Our snake have base items which build body:
class SnakeBodyElement: SKShapeNode {
var setChildSuperParent: (SKNode) -> Void = { _ in }
var diameter: CGFloat { 10 }
var runDuration: TimeInterval { 0.2 }
private var isNeedAddCild: Bool = false
private var childElement: SnakeBodyElement?
init(atPoint point: CGPoint) {
super.init()
self.path = CGPath(
ellipseIn: CGRect(x: 0, y: 0, width: diameter, height: diameter),
transform: nil
)
let pBody = SKPhysicsBody(
circleOfRadius: diameter / 2,
center: CGPoint(x: diameter / 2, y: diameter / 2)
)
pBody.isDynamic = true
pBody.categoryBitMask = CollisionMask.snakeBody.rawValue
pBody.contactTestBitMask = CollisionMask.edgeBody.rawValue | CollisionMask.apple.rawValue
self.physicsBody = pBody
self.fillColor = .systemTeal
self.strokeColor = .systemGreen
self.lineWidth = 1
self.position = point
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func removeFromParent() {
super.removeFromParent()
childElement?.removeFromParent()
}
func moveTo(position: CGPoint) {
let currentPosition = self.position
self.run(SKAction.move(to: position, duration: runDuration))
if let child = childElement {
child.moveTo(position: currentPosition)
} else if isNeedAddCild {
let newElement = SnakeBodyElement(atPoint: currentPosition)
newElement.setChildSuperParent = setChildSuperParent
childElement = newElement
setChildSuperParent(newElement)
isNeedAddCild = false
}
}
func eat() {
if let child = childElement {
child.eat()
} else {
isNeedAddCild = true
}
}
}
And a head:
final class SnakeHead: SnakeBodyElement {
override var diameter: CGFloat { 20 }
override var runDuration: TimeInterval { 1 }
private let moveSpeed: Double = 100
private var angle: CGFloat = 0
override init(atPoint point: CGPoint) {
super.init(atPoint: point)
self.name = "SnakeHead"
self.physicsBody?.categoryBitMask = CollisionMask.snakeHead.rawValue
self.physicsBody?.contactTestBitMask = CollisionMask.edgeBody.rawValue
| CollisionMask.apple.rawValue
| CollisionMask.snakeBody.rawValue
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func moveTo(position: CGPoint) {
let dx = CGFloat(moveSpeed) * sin(angle)
let dy = CGFloat(moveSpeed) * cos(angle)
let nextPosition = CGPoint(x: self.position.x + dx, y: self.position.y + dy)
super.moveTo(position: nextPosition)
}
func moveClockwise() {
angle += CGFloat.pi / 2
}
func moveCounterClockwise() {
angle -= CGFloat.pi / 2
}
}
SpriteKit provides a built-in game loop through the update(_:)
method in our scene. This is where you'll update the game's logic, such as moving the snake and checking for collisions.
override func update(_ currentTime: TimeInterval) {
// Add game logic here
}
To control the snake, you'll want to capture user input. One common way to do this is by handling touch events:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// Handle touch events to change the snake's direction
}
Now that you have the basic structure of game, it's time to implement the game logic. Here are some key steps:
In SpriteKit, games are built around scenes. We'll begin by designing our game scene, including the snake and food elements.
GameScene.swift
file in your Xcode project.final class GameScene: SKScene {
private var snake: SnakeHead?
private var apple: Apple?
let infoSender = CurrentValueSubject<Int, Never>(0)
override init(size: CGSize) {
super.init(size: size)
scaleMode = .resizeFill
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func didMove(to view: SKView) {
view.showsPhysics = true
guard let viewScene = view.scene else { return }
viewScene.physicsWorld.contactDelegate = self
viewScene.physicsBody?.allowsRotation = false
viewScene.backgroundColor = UIColor(red: 0.8, green: 0.8, blue: 0.8, alpha: 1)
viewScene.physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
viewScene.physicsBody?.categoryBitMask = CollisionMask.edgeBody.rawValue
viewScene.physicsBody?.collisionBitMask = CollisionMask.snakeBody.rawValue
| CollisionMask.snakeHead.rawValue
viewScene.physicsWorld.gravity = CGVector(dx: 0, dy: 0)
setup(skScene: viewScene)
}
override func update(_ currentTime: TimeInterval) {
if !isPaused {
snake?.moveTo(position: apple!.position)
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
buttonAction(touches, color: .green) { [weak self] tNode in
if tNode.name == "CounterClockwiseBtn" && !(self?.isPaused ?? true) {
self?.snake?.moveCounterClockwise()
} else if tNode.name == "ClockwiseBtn" && !(self?.isPaused ?? true) {
self?.snake?.moveClockwise()
} else if tNode.name == "Pause" {
self?.isPaused.toggle()
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
buttonAction(touches, color: .gray)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
isPaused = true
}
private func setup(skScene: SKScene) {
var position = CGPoint(x: skScene.frame.minX + 30, y: skScene.frame.minY + 30)
self.addChild(createBtn(name: "CounterClockwiseBtn", color: .gray, position: position))
position.x = skScene.frame.maxX - 80
self.addChild(createBtn(name: "ClockwiseBtn", color: .gray, position: position))
position.y = skScene.frame.maxY - 80
self.addChild(createBtn(name: "Pause", color: .systemBlue, position: position))
createSnake(skFrame: skScene.frame)
createApple(skFrame: skScene.frame)
}
private func createSnake(skFrame: CGRect) {
snake = SnakeHead(atPoint: CGPoint(x: skFrame.midX, y: skFrame.midY))
snake?.setChildSuperParent = self.addChild(_:)
self.addChild(snake!)
}
private func createApple(skFrame: CGRect) {
let randX = CGFloat(arc4random_uniform(UInt32(skFrame.maxX)) + 1)
let randY = CGFloat(arc4random_uniform(UInt32(skFrame.maxY)) + 1)
apple = Apple(position: CGPoint(x: randX, y: randY))
self.addChild(apple!)
}
private func createBtn(name: String?, color: UIColor, position: CGPoint) -> SKShapeNode {
let btn = SKShapeNode()
btn.path = CGPath(ellipseIn: CGRect(x: 0, y: 0, width: 45, height: 45), transform: nil)
btn.position = position
btn.fillColor = color
btn.strokeColor = .darkGray
btn.lineWidth = 10
btn.name = name
return btn
}
private func buttonAction(
_ touches: Set<UITouch>,
color: UIColor,
action: (SKShapeNode) -> Void = {_ in}
) {
for touch in touches {
let touchLocation = touch.location(in: self)
guard let touchedNode = self.atPoint(touchLocation) as? SKShapeNode,
touchedNode.name == "CounterClockwiseBtn"
|| touchedNode.name == "ClockwiseBtn"
|| touchedNode.name == "Pause"
else {
return
}
touchedNode.fillColor = color
action(touchedNode)
}
}
}
extension GameScene: SKPhysicsContactDelegate {
func didBegin(_ contact: SKPhysicsContact) {
let collisionObject: SKPhysicsBody
if contact.bodyA.node?.name == "SnakeHead" {
collisionObject = contact.bodyB
} else {
collisionObject = contact.bodyA
}
switch CollisionMask(rawValue: collisionObject.categoryBitMask) {
case .apple:
let apple = contact.bodyA.node is Apple ? contact.bodyA.node : contact.bodyB.node
snake?.eat()
apple?.removeFromParent()
infoSender.send(infoSender.value + 1)
createApple(skFrame: view?.scene?.frame ?? .zero)
case .edgeBody:
snake?.removeFromParent()
infoSender.send(0)
createSnake(skFrame: view?.scene?.frame ?? .zero)
default:
break
}
}
}
And finally, we'll update our application to show the score:
@main
struct SnakeApp: App {
private let scene: GameScene //BloxScene
@State private var info = "0"
var body: some Scene {
WindowGroup {
ZStack {
Color.gray
.edgesIgnoringSafeArea(.all)
VStack(spacing: 0) {
Text("Score: \(info)")
.font(.body)
.foregroundColor(.white)
SpriteView(
scene: scene,
debugOptions: [.showsFPS, .showsNodeCount, .showsDrawCount],
shouldRender: { timeInterval in true }
)
.frame(idealWidth: .infinity, idealHeight: .infinity)
}
}
.onReceive(scene.infoSender.eraseToAnyPublisher()) {
info = "\($0)"
}
}
}
init() {
self.scene = GameScene(size: CGSize(width: 10, height: 10))
}
}
Creating the classic "Snake" game using SpriteKit can be a fun and educational project for both beginners and experienced game developers. This article has provided a high-level overview of the steps involved in setting up the project and designing the game scene. The real challenge lies in implementing the game logic, handling user input, and creating an enjoyable gaming experience.
Remember that game development is an iterative process, so don't be discouraged by initial setbacks. With persistence and creativity, you can create a polished "Snake" game that is both engaging and entertaining. Good luck with your SpriteKit game development journey!