259 lines
11 KiB
Swift
259 lines
11 KiB
Swift
//
|
|
// GameScene.swift
|
|
// Privyet
|
|
//
|
|
// Created by Amy Bowersox on 5/23/20.
|
|
// Copyright © 2020 Erbosoft Metaverse Design Solutions. All rights reserved.
|
|
//
|
|
|
|
import SpriteKit
|
|
import GameplayKit
|
|
|
|
let BlockSize: CGFloat = 20.0
|
|
|
|
let TickLengthLevelOne = TimeInterval(600)
|
|
|
|
class GameScene: SKScene {
|
|
let gameLayer = SKNode()
|
|
let shapeLayer = SKNode()
|
|
let LayerPosition = CGPoint(x: 6, y: -6)
|
|
|
|
var tick: (() -> ())?
|
|
var tickLengthMillis = TickLengthLevelOne
|
|
var lastTick: NSDate?
|
|
|
|
var scaleFactor: CGFloat!
|
|
var holdControlRect: CGRect!
|
|
|
|
var textureCache = Dictionary<String, SKTexture>()
|
|
var textureAtlas: SKTextureAtlas?
|
|
|
|
required init(coder aDecoder: NSCoder) {
|
|
fatalError("NSCoder not supported")
|
|
}
|
|
|
|
override init(size: CGSize) {
|
|
super.init(size: size)
|
|
//print("Init scene with size \(size)")
|
|
|
|
textureAtlas = SKTextureAtlas(named: "Sprites")
|
|
|
|
anchorPoint = CGPoint(x: 0, y: 1.0)
|
|
|
|
let background = SKSpriteNode(imageNamed: "background")
|
|
//print("Background is sized \(background.size)")
|
|
|
|
// Scale computations
|
|
scaleFactor = CGFloat(min(size.width / background.size.width, size.height / background.size.height))
|
|
|
|
background.position = CGPoint(x: 0, y: 0)
|
|
background.anchorPoint = CGPoint(x: 0, y: 1.0)
|
|
background.setScale(scaleFactor)
|
|
addChild(background)
|
|
|
|
gameLayer.setScale(scaleFactor)
|
|
addChild(gameLayer)
|
|
|
|
// Load and add the game board and shape layer
|
|
let gameBoardTexture = SKTexture(imageNamed: "gameboard")
|
|
let gameBoard = SKSpriteNode(texture: gameBoardTexture, size: CGSize(width: BlockSize * CGFloat(NumColumns), height: BlockSize * CGFloat(NumRows)))
|
|
gameBoard.anchorPoint = CGPoint(x: 0, y: 1.0)
|
|
gameBoard.position = LayerPosition
|
|
shapeLayer.position = LayerPosition
|
|
shapeLayer.addChild(gameBoard)
|
|
gameLayer.addChild(shapeLayer)
|
|
|
|
// Calculate area of "hold" piece for hit-testing
|
|
let holdBBox = Shape.boundingBox(baseColumn: HoldColumn, baseRow: HoldRow)
|
|
let holdWidth = CGFloat(holdBBox.width) * BlockSize
|
|
let holdHeight = CGFloat(holdBBox.height) * BlockSize
|
|
let holdX = LayerPosition.x + (CGFloat(HoldColumn) * BlockSize)
|
|
let holdY = LayerPosition.y + (CGFloat(HoldRow) * BlockSize)
|
|
holdControlRect = CGRect(x: holdX * scaleFactor, y: holdY * scaleFactor, width: holdWidth * scaleFactor, height: holdHeight * scaleFactor)
|
|
|
|
// Set the theme music to play infinitely.
|
|
run(SKAction.repeatForever(SKAction.playSoundFileNamed("Sounds/theme.mp3", waitForCompletion: true)))
|
|
}
|
|
|
|
// Convenience function to play a sound file.
|
|
func playSound(sound: String) {
|
|
run(SKAction.playSoundFileNamed(sound, waitForCompletion: false))
|
|
}
|
|
|
|
// Called once per frame by the framework, before it's rendered.
|
|
override func update(_ currentTime: TimeInterval) {
|
|
guard let lastTick = lastTick else {
|
|
return
|
|
}
|
|
let timePassed = lastTick.timeIntervalSinceNow * -1000.0
|
|
if timePassed > tickLengthMillis {
|
|
self.lastTick = NSDate()
|
|
tick?()
|
|
}
|
|
}
|
|
|
|
// Starts the game tick going.
|
|
func startTicking() {
|
|
lastTick = NSDate()
|
|
}
|
|
|
|
// Stops the game tick.
|
|
func stopTicking() {
|
|
lastTick = nil
|
|
}
|
|
|
|
// Convert bucket row/column coordinates to display coordinates.
|
|
func pointForColumn(column: Int, row: Int) -> CGPoint {
|
|
let x = LayerPosition.x + (CGFloat(column) * BlockSize) + (BlockSize / 2)
|
|
let y = LayerPosition.y - ((CGFloat(row) * BlockSize) + (BlockSize / 2))
|
|
return CGPoint(x: x, y: y)
|
|
}
|
|
|
|
func rectForBucket() -> CGRect {
|
|
let width = CGFloat(NumColumns) * BlockSize
|
|
let height = CGFloat(NumRows) * BlockSize
|
|
return CGRect(x: (LayerPosition.x + (BlockSize / 2)) * scaleFactor, y: (LayerPosition.y + (BlockSize / 2)) * scaleFactor, width: width * scaleFactor, height: height * scaleFactor)
|
|
}
|
|
|
|
// Add the "preview" shape to the current scene. This also "rezzes in" a shape's block sprites.
|
|
func addPreviewShapeToScene(shape: Shape, completion: @escaping () -> ()) {
|
|
for block in shape.blocks {
|
|
var texture = textureCache[block.spriteName]
|
|
if texture == nil {
|
|
texture = textureAtlas?.textureNamed(block.spriteName)
|
|
textureCache[block.spriteName] = texture
|
|
}
|
|
let sprite = SKSpriteNode(texture: texture)
|
|
|
|
sprite.position = pointForColumn(column: block.column, row: block.row - 2)
|
|
shapeLayer.addChild(sprite)
|
|
block.sprite = sprite
|
|
|
|
// Animation
|
|
sprite.alpha = 0
|
|
|
|
let moveAction = SKAction.move(to: pointForColumn(column: block.column, row: block.row), duration: TimeInterval(0.2))
|
|
moveAction.timingMode = .easeOut
|
|
let fadeInAction = SKAction.fadeAlpha(to: 0.7, duration: 0.4)
|
|
fadeInAction.timingMode = .easeOut
|
|
sprite.run(SKAction.group([moveAction, fadeInAction]))
|
|
}
|
|
let waitAction = SKAction.wait(forDuration: 0.4)
|
|
let completeAction = SKAction.run(completion)
|
|
run(SKAction.sequence([waitAction, completeAction]))
|
|
}
|
|
|
|
// Move the "preview" shape into position so it becomes the new "falling" shape.
|
|
func movePreviewShape(shape: Shape, completion: @escaping () -> ()) {
|
|
for block in shape.blocks {
|
|
let sprite = block.sprite!
|
|
let moveTo = pointForColumn(column: block.column, row: block.row)
|
|
let moveToAction: SKAction = SKAction.move(to: moveTo, duration: 0.4)
|
|
moveToAction.timingMode = .easeOut
|
|
sprite.run(SKAction.group([moveToAction, SKAction.fadeAlpha(to: 1.0, duration: 0.2)]))
|
|
}
|
|
let waitAction = SKAction.wait(forDuration: 0.2)
|
|
let completeAction = SKAction.run(completion)
|
|
run(SKAction.sequence([waitAction, completeAction]))
|
|
}
|
|
|
|
// Redraw the falling shape by shifting the position of its component blocks.
|
|
func redrawShape(shape: Shape, completion: @escaping () -> ()) {
|
|
for block in shape.blocks {
|
|
let sprite = block.sprite!
|
|
let moveTo = pointForColumn(column: block.column, row: block.row)
|
|
let moveToAction: SKAction = SKAction.move(to: moveTo, duration: 0.05)
|
|
moveToAction.timingMode = .easeOut
|
|
sprite.run(moveToAction)
|
|
if block == shape.blocks.last {
|
|
sprite.run(SKAction.run(completion))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Animate the removed lines "exploding" and the remaining blocks dropping into place.
|
|
func animateCollapsingLines(linesToRemove: Array<Array<Block>>, fallenBlocks: Array<Array<Block>>, fadingShapes: Array<Shape>, completion: @escaping () -> ()) {
|
|
var longestDuration: TimeInterval = 0
|
|
|
|
// Animate the shapes fading out
|
|
for fadeShape in fadingShapes {
|
|
for block in fadeShape.blocks {
|
|
let sprite = block.sprite!
|
|
let fadeOutAction: SKAction = SKAction.fadeOut(withDuration: TimeInterval(0.1))
|
|
fadeOutAction.timingMode = .easeIn
|
|
sprite.run(SKAction.sequence([fadeOutAction, SKAction.removeFromParent()]))
|
|
}
|
|
}
|
|
|
|
// Animate the falling blocks dropping into place
|
|
for (columnIdx, column) in fallenBlocks.enumerated() {
|
|
for (blockIdx, block) in column.enumerated() {
|
|
let newPosition = pointForColumn(column: block.column, row: block.row)
|
|
let sprite = block.sprite!
|
|
|
|
let delay = (TimeInterval(columnIdx) * 0.05) + (TimeInterval(blockIdx) * 0.05)
|
|
let duration = TimeInterval(((sprite.position.y - newPosition.y) / BlockSize) * 0.1)
|
|
let moveAction = SKAction.move(to: newPosition, duration: duration)
|
|
moveAction.timingMode = .easeOut
|
|
sprite.run(SKAction.sequence([SKAction.wait(forDuration: delay), moveAction]))
|
|
longestDuration = max(longestDuration, duration + delay)
|
|
}
|
|
}
|
|
|
|
// Animate the "removed" blocks "exploding"
|
|
for rowToRemove in linesToRemove {
|
|
for block in rowToRemove {
|
|
let randomRadius = CGFloat(Float.random(in: 100.0 ... 500.0))
|
|
let goLeft = Bool.random()
|
|
|
|
var point = pointForColumn(column: block.column, row: block.row)
|
|
point = CGPoint(x: point.x + (goLeft ? -randomRadius : randomRadius), y: point.y)
|
|
|
|
let randomDuration = TimeInterval(Float.random(in: 0.5 ... 2.5))
|
|
|
|
var startAngle = CGFloat(Double.pi)
|
|
var endAngle = startAngle * 2
|
|
if (goLeft) {
|
|
endAngle = startAngle
|
|
startAngle = 0
|
|
}
|
|
|
|
let archPath = UIBezierPath(arcCenter: point, radius: randomRadius, startAngle: startAngle, endAngle: endAngle, clockwise: goLeft)
|
|
|
|
let archAction = SKAction.follow(archPath.cgPath, asOffset: false, orientToPath: true, duration: randomDuration)
|
|
archAction.timingMode = .easeIn
|
|
let sprite = block.sprite!
|
|
sprite.zPosition = 100
|
|
sprite.run(SKAction.sequence([
|
|
SKAction.group([archAction, SKAction.fadeOut(withDuration: TimeInterval(randomDuration))]),
|
|
SKAction.removeFromParent()]))
|
|
}
|
|
}
|
|
run(SKAction.sequence([SKAction.wait(forDuration: longestDuration), SKAction.run(completion)]))
|
|
}
|
|
|
|
// Animate the multiple-line clearance banner.
|
|
func animateClearanceBanner(bannerName: String?) {
|
|
guard let name = bannerName else {
|
|
return
|
|
}
|
|
var texture = textureCache[name]
|
|
if texture == nil {
|
|
texture = textureAtlas?.textureNamed(name)
|
|
textureCache[name] = texture
|
|
}
|
|
let sprite = SKSpriteNode(texture: texture)
|
|
|
|
sprite.position = CGPoint(x: LayerPosition.x + (CGFloat(NumColumns / 2) * BlockSize), y: LayerPosition.y - (CGFloat(NumRows / 2) * BlockSize))
|
|
sprite.zPosition = 20
|
|
sprite.alpha = 0
|
|
shapeLayer.addChild(sprite)
|
|
|
|
// Animate a quick fade-in followed by a slow fade out
|
|
let fadeInAction: SKAction = SKAction.fadeAlpha(to: 0.9, duration: 0.05)
|
|
let fadeOutAction: SKAction = SKAction.fadeOut(withDuration: 1.5)
|
|
fadeOutAction.timingMode = .easeIn
|
|
sprite.run(SKAction.sequence([fadeInAction, fadeOutAction, SKAction.removeFromParent()]))
|
|
}
|
|
}
|