KEMBAR78
2048 on swift | PPTX
2048 on Swift
07.28
Introduction
Kohei Iwasaki
14 years - Game programming (HSP, C/C++, Perl, Direct2D)
20 years - Web applications (Ruby on Rails, Titanium)
22 years - Life is Tech! education (Rails, Objective-C)
23 years - Donuts (PHP, Rails, Swift, Go, etc)
25 years - Antelos Director/CTO
2
< New!
Purpose of this workshop
The main purpose of this workshop is to learn how to create an iOS
application.
・How to use Xcode
・The architecture of iOS Applications
・Professional way of creating apps
Source code: https://github.com/antelosdev/swift2048
3
What’s 2048?
4
You merge similar tiles by moving them in
any of the four directions to make "bigger"
tiles.
After each move, a new tile appears at
random empty position with value of either 2
or 4.
The game is over when all the boxes are
filled and there are no moves that can
merge tiles.
Xcode
5
Open Xcode
To create iOS applications, you usually use Xcode. Open Xcode and
start creating a new project as shown in the figures below.
6
Create your project
Create your new project.
Next, create the project to wherever you want.
7
Xcode window
8
4 Steps to create 2048
1. Layout two screens: Start screen and Game screen.
You’ll learn how to use Interface Builder and Auto Layout.
2. Write code to create Tiles and put them on the board.
You’ll learn how to create and add views with code.
3. Think about the algorithm of 2048.
You’ll learn how to manage the game logics.
4. Handle user swipes and update the game board.
You’ll learn how to handle swipes and to update views.
9
1. Layout
10
Storyboard
Open Main.storyboard file and put a Button to the view.
11
Design the start button
You can select the attributes as shown in the figure below.
12
Auto Layout
Auto Layout dynamically calculates the size and position of all the
views in your view hierarchy, based on constraints placed on those
views.
Add size and position constraints to the start button.
13
Run on the simulator
Choose iPhone6 (or others will also be ok) and push the Run button or
Command+R.
14
Add the next screen
Add the next screen by putting a View Controller. Then control+drag
from the start button to the new screen and select “Present Modally”.
This makes a transition to the next screen when the user taps the
button. It is called “Segue”.
15
Add Game Board View
In the second screen, we add a View as a game board. Change the
background color to make its position be clear.
16
Add constraints
Add 4 constraints: Leading Space to 20, Trailing Space to 20, Aspect
Ratio to 1:1, Center Horizontal and Center Vertically in Container.
17
Quiz: Fix constraints
For now, the display gets corrupted when you rotate the device. How
can you make it fit within the display?
18
To fix the constraints
You can fix constraints in Size Inspector shown in the figure below.
The sign of inequality and the priority of the constraint also can be
changed.
19
Answer
20
2. Write Code
21
swift tutorial
Event-driven
In GUI applications like iOS/Android apps, the flow of the program is
determined by events such as user actions (taps, swipes), sensor
outputs, or messages from the OS.
22
Some Action
Tap
MVC architecture
Model-View-Controller (MVC) is a software architectural pattern that
divides an application into three components:
Model manages data, logic and rules of the application.
View renders the output to the user.
Controller accepts input and converts it to commands for the Model or
View.
23
Create Groups
Create new groups to make
application structure be as:
Assets: images, sounds, movies
Layouts: storyboard, xib※ files
Models, Views, Controllers: main
application structure based on MVC
Config: Application settings files
24
※ xib is a XML based format used for the UI of view components at design time.
Create GameViewController
First, create a new controller class to Controllers group to handle the
screen you added in the Storyboard.
25
GameViewController
This class has a boardView you put in the Storyboard.
@IBOutlet means that this variable can be referred to in Interface
Builder such as Storyboard.
26
import UIKit
class GameViewController: UIViewController {
@IBOutlet weak var boardView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
}
}
Set the class on the Storyboard
Then set the class of the View Controller in the Storyboard to
GameViewController.
27
Make boardView referred
Create a new referencing outlet to boardView be referred.
28
Create TileLabel
Next, create UILabel named TileLabel to Views group.
The UILabel class implements a read-only text view.
You can use TileLabel to show the tile with the number.
29
TileLabel
TileLabel is a View with the rounded corners of 8px, there is a black
border of 1px. The text will be displayed in the center.
30
import UIKit
class TileLabel: UILabel {
var position : (x:Int, y:Int)!
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.borderWidth = 1.0
self.layer.borderColor = UIColor.blackColor().CGColor
self.layer.cornerRadius = 8.0
self.layer.masksToBounds = true
self.textAlignment = .Center
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
UIViewController LifeCycle
Put TileLabels here so that boardView’s
size and position must be decided before.
31
ViewWillLayoutSubviews
ViewDidLayoutSubviews
ViewDidAppear
loadView
viewDidLoad
viewWillAppear
ViewWillDisappear
ViewDidDisappear
Add TileLabel
Add a TileLabel to the boardView. The frame indicates the position and
size of the TileLabel.
32
import UIKit
class GameViewController: UIViewController {
@IBOutlet weak var boardView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let tileLabel = TileLabel(frame: CGRect(x: 0, y: 0, width: 40, height: 40))
self.boardView.addSubview(tileLabel)
}
}
Quiz: Put 16 tiles to the boardView
The result should be as shown in the figure below. One tile is put on
the boardView. Now, how can you put 4x4=16 tiles on the boardView?
33
Answer
Calculate the position and size of the tile.
34
import UIKit
class GameViewController: UIViewController {
// ...
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let tileSize = self.boardView.frame.size.width / 4
for y in 0..<4 {
for x in 0..<4 {
let tileLabel = TileLabel(frame: CGRect(
x: CGFloat(x)*tileSize,
y: CGFloat(y)*tileSize,
width: tileSize,
height: tileSize
))
tileLabel.position = (x: x, y: y)
self.boardView.addSubview(tileLabel)
}
}
}
}
3. Game Algorithm
35
Create Game model
Now it’s time to create game logic. To manage the game logic, create
Game model.
36
Game model
In this application, Game model exists only one. You can use Singleton
Pattern to ensure that (sharedInstance).
Board status can be represented as a multidimensional array.
37
import UIKit
class Game: NSObject {
static var sharedInstance = Game()
let boardSize = 4
var board = [[Int]]()
dynamic var turn = 0
override init() {
super.init()
for _ in 0..<self.boardSize {
self.board.append([Int](count: self.boardSize, repeatedValue: 0))
}
}
}
Game model methods
1. Get empty tile positions of the board.
2. Generate “2” or “4” at random space of the board.
3. Slide a line of the board.
4. Slide the board.
5. Check if the game is over or not.
38
Get empty tile positions
To generate number on the board, first create a method to get empty
positions of the board. This returns an array of coordinates Tuple.
39
class Game: NSObject {
// ...
func getEmptyPositions() -> [(x:Int, y:Int)]{
var results = [(x:Int, y:Int)]()
for y in 0..<self.boardSize {
for x in 0..<self.boardSize {
if(self.board[y][x] == 0){
results.append((x, y))
}
}
}
return results
}
}
Generate “2” or “4"
Then generate “2” or “4” at the random position of the board.
arc4random_uniform makes random number.
ex) arc4random_uniform(5) returns 0-4 randomly.
40
class Game: NSObject {
// ...
func generateNumber() {
let empties = self.getEmptyPositions()
let randomNum = arc4random_uniform(UInt32(empties.count))
let randomPos = empties[Int(randomNum)]
// Set 2 or 4
self.board[randomPos.y][randomPos.x] = Int(arc4random_uniform(2)+1) * 2
}
func nextTurn() {
self.generateNumber()
self.turn += 1
}
}
Quiz: How to create slide function?
For example, [2, 0, 4, 4] must be [2, 8, 0, 0] when the board slides to
left.
41
Answer
Create these methods to create slide function.
1. Condense [2, 2, 0, 4] => [2, 2, 4]
2. Merge [2, 2, 4] => [4, 4]
3. Fill blanks [4, 4, 0, 0]
Then, create SlideNumbers method using these methods.
42
Condense
Condense method uses filter method to exclude 0 from the received
numbers.
43
class Game: NSObject {
// ...
// [2, 2, 0, 4] => [2, 2, 4]
func condense(numbers: [Int]) -> [Int] {
return numbers.filter { (num) -> Bool in
return num != 0
}
}
}
Merge
Merge method merges two adjacent numbers.
44
class Game: NSObject {
// ...
// [2, 2, 4] => [4, 4]
func merge(numbers: [Int]) -> [Int] {
var mergedNumbers = [Int]()
var i = 0
while i < numbers.count {
if i+1 == numbers.count {
mergedNumbers.append(numbers[i])
break
}
if numbers[i] == numbers[i+1] {
mergedNumbers.append(numbers[i] * 2)
i += 2
} else {
mergedNumbers.append(numbers[i])
i += 1
}
}
return mergedNumbers
}
}
Fill Blanks
FillBlanks method fills the blanks with 0.
45
class Game: NSObject {
// ...
// [4, 4] => [4, 4, 0, 0]
func fillBlanks(numbers: [Int]) -> [Int] {
return numbers + [Int](count: self.boardSize-numbers.count, repeatedValue: 0)
}
}
Slide Numbers
SlideNumbers method uses the three methods. If the direction is “up”
or “left”, the numbers should be reversed before and after the
processing.
[2, 2, 0, 4] => [4, 0, 2, 2] => [4, 4, 0, 0] => [0, 0, 4, 4]
46
class Game: NSObject {
// ...
// reverse=false(up, left) : [2, 2, 0, 4] => [4, 4, 0, 0]
// reverse=true(right, down) : [2, 2, 0, 4] => [0, 0, 4, 4]
func slideNumbers(numbers: [Int], reverse: Bool) -> [Int] {
if reverse {
return fillBlanks(merge(condense(numbers.reverse()))).reverse()
} else {
return fillBlanks(merge(condense(numbers)))
}
}
}
reverse processing reverse again
Slide Board
SlideBoard method slides all lines of the board considering the
direction.
47
class Game: NSObject {
// ...
func slideBoard(dir: UISwipeGestureRecognizerDirection, virtual: Bool) -> [[Int]] {
var slidBoard = self.board
for i in 0..<self.boardSize {
switch dir {
case UISwipeGestureRecognizerDirection.Up:
let slid = slideNumbers(board.map({ (line) -> Int in return line[i] }), reverse: false)
for t in 0..<self.boardSize {
slidBoard[t][i] = slid[t]
}
case UISwipeGestureRecognizerDirection.Left:
slidBoard[i] = slideNumbers(board[i], reverse: false)
case UISwipeGestureRecognizerDirection.Right:
slidBoard[i] = slideNumbers(board[i], reverse: true)
case UISwipeGestureRecognizerDirection.Down:
let slid = slideNumbers(board.map({ (line) -> Int in return line[i] }), reverse: true)
for t in 0..<self.boardSize {
slidBoard[t][i] = slid[t]
}
default:
print("unexpected direction")
}
}
if !virtual{
self.board = slidBoard
self.nextTurn()
}
return slidBoard
}
}
Check if the game is over
To check if the game is over or not, create a method to get swipable
directions.
48
class Game: NSObject {
// ...
func swipableDirections() -> [UISwipeGestureRecognizerDirection]{
var results = [UISwipeGestureRecognizerDirection]()
for dir:UISwipeGestureRecognizerDirection in [.Left, .Down, .Right, .Up]{
let slidBoard = slideBoard(dir, virtual:true)
for i in 0..<self.boardSize {
if !self.board[i].elementsEqual(slidBoard[i]) {
results.append(dir)
break
}
}
}
return results
}
func isGameOver() -> Bool {
return self.swipableDirections().count == 0
}
}
4. Handle Swipes
& Update Views
49
Fix viewDidLayoutSubviews
Fix viewDidLayoutSubviews method to handle the difference board
size and call nextTurn() to start first turn of the game.
50
class GameViewController: UIViewController {
// ...
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let tileSize = self.boardView.frame.size.width / CGFloat(Game.sharedInstance.boardSize)
for y in 0..<Game.sharedInstance.boardSize {
for x in 0..<Game.sharedInstance.boardSize {
let tileLabel = TileLabel(frame: CGRect(
x: CGFloat(x)*tileSize,
y: CGFloat(y)*tileSize,
width: tileSize,
height: tileSize
))
tileLabel.position = (x: x, y: y)
self.boardView.addSubview(tileLabel)
}
}
Game.sharedInstance.nextTurn()
}
}
Handle swipes
Use SwipeGestureRecognizer to handle swipes.
51
class GameViewController: UIViewController {
@IBOutlet weak var boardView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
for dir:UISwipeGestureRecognizerDirection in [.Up, .Left, .Right, .Down] {
let sgr = UISwipeGestureRecognizer(target: self, action: #selector(GameViewController.swiped(_:)))
sgr.direction = dir
self.view.addGestureRecognizer(sgr)
}
}
func swiped(sender: UISwipeGestureRecognizer) {
if Game.sharedInstance.swipableDirections().contains(sender.direction) {
Game.sharedInstance.slideBoard(sender.direction, virtual: false)
}
if Game.sharedInstance.isGameOver() {
print("Game Over.")
}
}
// ...
}
Update Views
Use KVO(key-value observing) to update TileLabel’s number.
Using KVO, when the specified variable (Game turn) changed, the
registered observers (TileLabels) receive an notification.
52
Game model
dynamic var turn = 0
TileLabel
Observe
Fire event when
turn changed
KVO
Call addObserver and removeObserver method to use KVO.
Forgetting to remove observer causes a memory issue.
53
class TileLabel: UILabel {
var position : (x:Int, y:Int)!
override init(frame: CGRect) {
super.init(frame: frame)
// ...
Game.sharedInstance.addObserver(self, forKeyPath: "turn", options: NSKeyValueObservingOptions(), context: nil)
}
// When the turn of the Game changed, this method is called
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context:
UnsafeMutablePointer<Void>) {
// Update the text here.
}
// Do not forget to removeObserver here
deinit {
Game.sharedInstance.removeObserver(self, forKeyPath: "turn")
}
}
Set text and background color
Decide the background color and set the text.
54
class TileLabel: UILabel {
// ...
// When the turn of the Game changed, this method is called
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context:
UnsafeMutablePointer<Void>) {
self.setNumber(Game.sharedInstance.board[self.position.y][self.position.x])
}
// Decide the color
func getHue(num:Int, hue:CGFloat = 60) -> CGFloat{
if(num < 2){ return hue }
if(hue == 360){ return getHue(num / 2, hue: 24 ) }
return getHue(num / 2, hue: hue + 24 )
}
func setNumber(num:Int){
if num == 0 {
self.text = ""
self.backgroundColor = UIColor.clearColor()
} else {
self.text = "(num)"
self.backgroundColor = UIColor(hue: getHue(num)/360, saturation: 1.0, brightness: 1.0, alpha: 1.0)
}
}
}
Congratulation!
55
Now, your 2048 application is complete!
Run the simulator and play the game ;)
You can change the boardSize to 5.
If you have any question, ask me anytime.
https://www.facebook.com/kohei.iwasaki.1

2048 on swift

  • 1.
  • 2.
    Introduction Kohei Iwasaki 14 years- Game programming (HSP, C/C++, Perl, Direct2D) 20 years - Web applications (Ruby on Rails, Titanium) 22 years - Life is Tech! education (Rails, Objective-C) 23 years - Donuts (PHP, Rails, Swift, Go, etc) 25 years - Antelos Director/CTO 2 < New!
  • 3.
    Purpose of thisworkshop The main purpose of this workshop is to learn how to create an iOS application. ・How to use Xcode ・The architecture of iOS Applications ・Professional way of creating apps Source code: https://github.com/antelosdev/swift2048 3
  • 4.
    What’s 2048? 4 You mergesimilar tiles by moving them in any of the four directions to make "bigger" tiles. After each move, a new tile appears at random empty position with value of either 2 or 4. The game is over when all the boxes are filled and there are no moves that can merge tiles.
  • 5.
  • 6.
    Open Xcode To createiOS applications, you usually use Xcode. Open Xcode and start creating a new project as shown in the figures below. 6
  • 7.
    Create your project Createyour new project. Next, create the project to wherever you want. 7
  • 8.
  • 9.
    4 Steps tocreate 2048 1. Layout two screens: Start screen and Game screen. You’ll learn how to use Interface Builder and Auto Layout. 2. Write code to create Tiles and put them on the board. You’ll learn how to create and add views with code. 3. Think about the algorithm of 2048. You’ll learn how to manage the game logics. 4. Handle user swipes and update the game board. You’ll learn how to handle swipes and to update views. 9
  • 10.
  • 11.
    Storyboard Open Main.storyboard fileand put a Button to the view. 11
  • 12.
    Design the startbutton You can select the attributes as shown in the figure below. 12
  • 13.
    Auto Layout Auto Layoutdynamically calculates the size and position of all the views in your view hierarchy, based on constraints placed on those views. Add size and position constraints to the start button. 13
  • 14.
    Run on thesimulator Choose iPhone6 (or others will also be ok) and push the Run button or Command+R. 14
  • 15.
    Add the nextscreen Add the next screen by putting a View Controller. Then control+drag from the start button to the new screen and select “Present Modally”. This makes a transition to the next screen when the user taps the button. It is called “Segue”. 15
  • 16.
    Add Game BoardView In the second screen, we add a View as a game board. Change the background color to make its position be clear. 16
  • 17.
    Add constraints Add 4constraints: Leading Space to 20, Trailing Space to 20, Aspect Ratio to 1:1, Center Horizontal and Center Vertically in Container. 17
  • 18.
    Quiz: Fix constraints Fornow, the display gets corrupted when you rotate the device. How can you make it fit within the display? 18
  • 19.
    To fix theconstraints You can fix constraints in Size Inspector shown in the figure below. The sign of inequality and the priority of the constraint also can be changed. 19
  • 20.
  • 21.
  • 22.
    Event-driven In GUI applicationslike iOS/Android apps, the flow of the program is determined by events such as user actions (taps, swipes), sensor outputs, or messages from the OS. 22 Some Action Tap
  • 23.
    MVC architecture Model-View-Controller (MVC)is a software architectural pattern that divides an application into three components: Model manages data, logic and rules of the application. View renders the output to the user. Controller accepts input and converts it to commands for the Model or View. 23
  • 24.
    Create Groups Create newgroups to make application structure be as: Assets: images, sounds, movies Layouts: storyboard, xib※ files Models, Views, Controllers: main application structure based on MVC Config: Application settings files 24 ※ xib is a XML based format used for the UI of view components at design time.
  • 25.
    Create GameViewController First, createa new controller class to Controllers group to handle the screen you added in the Storyboard. 25
  • 26.
    GameViewController This class hasa boardView you put in the Storyboard. @IBOutlet means that this variable can be referred to in Interface Builder such as Storyboard. 26 import UIKit class GameViewController: UIViewController { @IBOutlet weak var boardView: UIView! override func viewDidLoad() { super.viewDidLoad() } }
  • 27.
    Set the classon the Storyboard Then set the class of the View Controller in the Storyboard to GameViewController. 27
  • 28.
    Make boardView referred Createa new referencing outlet to boardView be referred. 28
  • 29.
    Create TileLabel Next, createUILabel named TileLabel to Views group. The UILabel class implements a read-only text view. You can use TileLabel to show the tile with the number. 29
  • 30.
    TileLabel TileLabel is aView with the rounded corners of 8px, there is a black border of 1px. The text will be displayed in the center. 30 import UIKit class TileLabel: UILabel { var position : (x:Int, y:Int)! override init(frame: CGRect) { super.init(frame: frame) self.layer.borderWidth = 1.0 self.layer.borderColor = UIColor.blackColor().CGColor self.layer.cornerRadius = 8.0 self.layer.masksToBounds = true self.textAlignment = .Center } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } }
  • 31.
    UIViewController LifeCycle Put TileLabelshere so that boardView’s size and position must be decided before. 31 ViewWillLayoutSubviews ViewDidLayoutSubviews ViewDidAppear loadView viewDidLoad viewWillAppear ViewWillDisappear ViewDidDisappear
  • 32.
    Add TileLabel Add aTileLabel to the boardView. The frame indicates the position and size of the TileLabel. 32 import UIKit class GameViewController: UIViewController { @IBOutlet weak var boardView: UIView! override func viewDidLoad() { super.viewDidLoad() } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() let tileLabel = TileLabel(frame: CGRect(x: 0, y: 0, width: 40, height: 40)) self.boardView.addSubview(tileLabel) } }
  • 33.
    Quiz: Put 16tiles to the boardView The result should be as shown in the figure below. One tile is put on the boardView. Now, how can you put 4x4=16 tiles on the boardView? 33
  • 34.
    Answer Calculate the positionand size of the tile. 34 import UIKit class GameViewController: UIViewController { // ... override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() let tileSize = self.boardView.frame.size.width / 4 for y in 0..<4 { for x in 0..<4 { let tileLabel = TileLabel(frame: CGRect( x: CGFloat(x)*tileSize, y: CGFloat(y)*tileSize, width: tileSize, height: tileSize )) tileLabel.position = (x: x, y: y) self.boardView.addSubview(tileLabel) } } } }
  • 35.
  • 36.
    Create Game model Nowit’s time to create game logic. To manage the game logic, create Game model. 36
  • 37.
    Game model In thisapplication, Game model exists only one. You can use Singleton Pattern to ensure that (sharedInstance). Board status can be represented as a multidimensional array. 37 import UIKit class Game: NSObject { static var sharedInstance = Game() let boardSize = 4 var board = [[Int]]() dynamic var turn = 0 override init() { super.init() for _ in 0..<self.boardSize { self.board.append([Int](count: self.boardSize, repeatedValue: 0)) } } }
  • 38.
    Game model methods 1.Get empty tile positions of the board. 2. Generate “2” or “4” at random space of the board. 3. Slide a line of the board. 4. Slide the board. 5. Check if the game is over or not. 38
  • 39.
    Get empty tilepositions To generate number on the board, first create a method to get empty positions of the board. This returns an array of coordinates Tuple. 39 class Game: NSObject { // ... func getEmptyPositions() -> [(x:Int, y:Int)]{ var results = [(x:Int, y:Int)]() for y in 0..<self.boardSize { for x in 0..<self.boardSize { if(self.board[y][x] == 0){ results.append((x, y)) } } } return results } }
  • 40.
    Generate “2” or“4" Then generate “2” or “4” at the random position of the board. arc4random_uniform makes random number. ex) arc4random_uniform(5) returns 0-4 randomly. 40 class Game: NSObject { // ... func generateNumber() { let empties = self.getEmptyPositions() let randomNum = arc4random_uniform(UInt32(empties.count)) let randomPos = empties[Int(randomNum)] // Set 2 or 4 self.board[randomPos.y][randomPos.x] = Int(arc4random_uniform(2)+1) * 2 } func nextTurn() { self.generateNumber() self.turn += 1 } }
  • 41.
    Quiz: How tocreate slide function? For example, [2, 0, 4, 4] must be [2, 8, 0, 0] when the board slides to left. 41
  • 42.
    Answer Create these methodsto create slide function. 1. Condense [2, 2, 0, 4] => [2, 2, 4] 2. Merge [2, 2, 4] => [4, 4] 3. Fill blanks [4, 4, 0, 0] Then, create SlideNumbers method using these methods. 42
  • 43.
    Condense Condense method usesfilter method to exclude 0 from the received numbers. 43 class Game: NSObject { // ... // [2, 2, 0, 4] => [2, 2, 4] func condense(numbers: [Int]) -> [Int] { return numbers.filter { (num) -> Bool in return num != 0 } } }
  • 44.
    Merge Merge method mergestwo adjacent numbers. 44 class Game: NSObject { // ... // [2, 2, 4] => [4, 4] func merge(numbers: [Int]) -> [Int] { var mergedNumbers = [Int]() var i = 0 while i < numbers.count { if i+1 == numbers.count { mergedNumbers.append(numbers[i]) break } if numbers[i] == numbers[i+1] { mergedNumbers.append(numbers[i] * 2) i += 2 } else { mergedNumbers.append(numbers[i]) i += 1 } } return mergedNumbers } }
  • 45.
    Fill Blanks FillBlanks methodfills the blanks with 0. 45 class Game: NSObject { // ... // [4, 4] => [4, 4, 0, 0] func fillBlanks(numbers: [Int]) -> [Int] { return numbers + [Int](count: self.boardSize-numbers.count, repeatedValue: 0) } }
  • 46.
    Slide Numbers SlideNumbers methoduses the three methods. If the direction is “up” or “left”, the numbers should be reversed before and after the processing. [2, 2, 0, 4] => [4, 0, 2, 2] => [4, 4, 0, 0] => [0, 0, 4, 4] 46 class Game: NSObject { // ... // reverse=false(up, left) : [2, 2, 0, 4] => [4, 4, 0, 0] // reverse=true(right, down) : [2, 2, 0, 4] => [0, 0, 4, 4] func slideNumbers(numbers: [Int], reverse: Bool) -> [Int] { if reverse { return fillBlanks(merge(condense(numbers.reverse()))).reverse() } else { return fillBlanks(merge(condense(numbers))) } } } reverse processing reverse again
  • 47.
    Slide Board SlideBoard methodslides all lines of the board considering the direction. 47 class Game: NSObject { // ... func slideBoard(dir: UISwipeGestureRecognizerDirection, virtual: Bool) -> [[Int]] { var slidBoard = self.board for i in 0..<self.boardSize { switch dir { case UISwipeGestureRecognizerDirection.Up: let slid = slideNumbers(board.map({ (line) -> Int in return line[i] }), reverse: false) for t in 0..<self.boardSize { slidBoard[t][i] = slid[t] } case UISwipeGestureRecognizerDirection.Left: slidBoard[i] = slideNumbers(board[i], reverse: false) case UISwipeGestureRecognizerDirection.Right: slidBoard[i] = slideNumbers(board[i], reverse: true) case UISwipeGestureRecognizerDirection.Down: let slid = slideNumbers(board.map({ (line) -> Int in return line[i] }), reverse: true) for t in 0..<self.boardSize { slidBoard[t][i] = slid[t] } default: print("unexpected direction") } } if !virtual{ self.board = slidBoard self.nextTurn() } return slidBoard } }
  • 48.
    Check if thegame is over To check if the game is over or not, create a method to get swipable directions. 48 class Game: NSObject { // ... func swipableDirections() -> [UISwipeGestureRecognizerDirection]{ var results = [UISwipeGestureRecognizerDirection]() for dir:UISwipeGestureRecognizerDirection in [.Left, .Down, .Right, .Up]{ let slidBoard = slideBoard(dir, virtual:true) for i in 0..<self.boardSize { if !self.board[i].elementsEqual(slidBoard[i]) { results.append(dir) break } } } return results } func isGameOver() -> Bool { return self.swipableDirections().count == 0 } }
  • 49.
    4. Handle Swipes &Update Views 49
  • 50.
    Fix viewDidLayoutSubviews Fix viewDidLayoutSubviewsmethod to handle the difference board size and call nextTurn() to start first turn of the game. 50 class GameViewController: UIViewController { // ... override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() let tileSize = self.boardView.frame.size.width / CGFloat(Game.sharedInstance.boardSize) for y in 0..<Game.sharedInstance.boardSize { for x in 0..<Game.sharedInstance.boardSize { let tileLabel = TileLabel(frame: CGRect( x: CGFloat(x)*tileSize, y: CGFloat(y)*tileSize, width: tileSize, height: tileSize )) tileLabel.position = (x: x, y: y) self.boardView.addSubview(tileLabel) } } Game.sharedInstance.nextTurn() } }
  • 51.
    Handle swipes Use SwipeGestureRecognizerto handle swipes. 51 class GameViewController: UIViewController { @IBOutlet weak var boardView: UIView! override func viewDidLoad() { super.viewDidLoad() for dir:UISwipeGestureRecognizerDirection in [.Up, .Left, .Right, .Down] { let sgr = UISwipeGestureRecognizer(target: self, action: #selector(GameViewController.swiped(_:))) sgr.direction = dir self.view.addGestureRecognizer(sgr) } } func swiped(sender: UISwipeGestureRecognizer) { if Game.sharedInstance.swipableDirections().contains(sender.direction) { Game.sharedInstance.slideBoard(sender.direction, virtual: false) } if Game.sharedInstance.isGameOver() { print("Game Over.") } } // ... }
  • 52.
    Update Views Use KVO(key-valueobserving) to update TileLabel’s number. Using KVO, when the specified variable (Game turn) changed, the registered observers (TileLabels) receive an notification. 52 Game model dynamic var turn = 0 TileLabel Observe Fire event when turn changed
  • 53.
    KVO Call addObserver andremoveObserver method to use KVO. Forgetting to remove observer causes a memory issue. 53 class TileLabel: UILabel { var position : (x:Int, y:Int)! override init(frame: CGRect) { super.init(frame: frame) // ... Game.sharedInstance.addObserver(self, forKeyPath: "turn", options: NSKeyValueObservingOptions(), context: nil) } // When the turn of the Game changed, this method is called override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) { // Update the text here. } // Do not forget to removeObserver here deinit { Game.sharedInstance.removeObserver(self, forKeyPath: "turn") } }
  • 54.
    Set text andbackground color Decide the background color and set the text. 54 class TileLabel: UILabel { // ... // When the turn of the Game changed, this method is called override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) { self.setNumber(Game.sharedInstance.board[self.position.y][self.position.x]) } // Decide the color func getHue(num:Int, hue:CGFloat = 60) -> CGFloat{ if(num < 2){ return hue } if(hue == 360){ return getHue(num / 2, hue: 24 ) } return getHue(num / 2, hue: hue + 24 ) } func setNumber(num:Int){ if num == 0 { self.text = "" self.backgroundColor = UIColor.clearColor() } else { self.text = "(num)" self.backgroundColor = UIColor(hue: getHue(num)/360, saturation: 1.0, brightness: 1.0, alpha: 1.0) } } }
  • 55.
    Congratulation! 55 Now, your 2048application is complete! Run the simulator and play the game ;) You can change the boardSize to 5. If you have any question, ask me anytime. https://www.facebook.com/kohei.iwasaki.1

Editor's Notes

  • #3 ゲームプログラマーを目指していたため、大学1年まではC/C++といったいわゆる低級言語をメインに扱っていた。Webの世界に入ったのは大学2年の時。スタートアップで半年間Ruby言語を使ったWeb開発やiOS/Androidアプリ開発を習得。その後の長期インターンで複数社の大企業を経験した。4年の際はLife is Tech!でiOSスクールの立ち上げなどを行い、100人以上の中高生にプログラミングを教えた。生徒に楽しんで学んでもらうことや、教科書の執筆などを経験した。ベンチャーに就職し、数百万人規模のWebサービスを一人で開発・運営、また、新規サービスを立ち上げから100万人規模まで一人で開発を行い売却まで行った経験がある。大学時代から勉強会やハッカソンも多数開催しており、DeNAとYahoo!へ行った同期(現在は二人ともスタートアップへ移動)と一緒に「Donuts / DeNA / Y社でマリオメーカーをプレイする勉強会」などの開催も。現在はAntelos株式会社のCTOとして複数のサービスの開発や、メンバーの教育を行っている。
  • #4 この勉強会の目的は、きちんとしたアプリを、プロのやり方に沿って作ること。 入門書を読めば学べるような内容だけで終わらせず、実践的なスキルを紹介する。 後半から中級者向けの内容が含まれており、この短い時間内で完全な紹介をすることはできないが、コードを写経していくとアプリは完成するのでわからない部分は家に帰った後にじっくり復習してもらいたい。また、質問はFacebookなどで投げてもらえば対応する。
  • #5 完成したアプリでデモを見せる。
  • #7 図の通り進む。万が一わからない場合は周りのメンターを呼んでもらう。
  • #8 アプリ名をswift2048に、LanguageをSwiftに。
  • #9 Xcodeは主に4つのエリアで構成されている。Tool barエリア、Navigatorエリア、Editorエリア、Utilitiesエリア。
  • #10 4つのステップで2048を作っていく。まずは、スタート画面とゲーム画面の2つの画面のレイアウトを作っていく。ここではまだプログラムを書かない。インターフェースビルダーと呼ばれるツールと、オートレイアウトという機能について学ぶ。 次に、Swiftについて軽く勉強し、実際にコードで数字を表示するタイルを作って盤面に配置する。 次が今回最も難しい部分で、2048のロジックを実装する。例えば、ユーザーが画面をスワイプした時の動作や、ゲームオーバーの判定など。 最後にユーザーのスワイプをハンドリングして、ゲームの盤面を更新する。
  • #11 ではまずはレイアウトから。以後、実際に前で作りながら進行していく。
  • #12 Storyboardというインターフェースビルダーを使うと、コードを書かずにUI(アプリの見た目)を作る事ができる。まずはボタンを配置してみる。(ユーティリティエリアの下部、左から3番目のボタンでUI部品一覧を出す事ができる)
  • #13 ボタンの見た目を作る。”Button”をダブルクリックしてテキストを「START」に変える。 また、引っ張ってサイズを調整する。ボタンは画面の真ん中に配置する。 ボタンの文字色や背景色はユーティリティエリアで設定できる。
  • #14 ビューの位置やサイズを決定するためには、オートレイアウトという機能を使う。これを使うと、iPhone4、iPhone5、iPhone6、どんなサイズのデバイスでもビューが意図通り表示することができる。まずは、スタートボタンが画面の中央になるように制約を追加する。次に、幅と高さを固定する制約を追加する。
  • #15 シミュレーターで確認してみる。
  • #16 次に、スタートボタンを押した後に表示される、実際にゲームをプレイする画面を作成する。 UI部品の一覧からビューコントローラーを画面に配置する。 次に、STARTボタンを押した時にその画面に遷移するように、セグエを追加する。
  • #17 ゲーム画面に盤面を配置する。Viewを探して配置。背景が白でわかりにくいので、色を変える。正方形になるよう、真ん中に配置する。
  • #18 次に、左右からの距離がそれぞれ20、縦横比が1:1、中心に配置されるように制約を追加する。
  • #19 ここで実行すると、デバイスが縦向きのときには正しく表示されるが、横向きにすると盤面がはみ出てしまうのがわかる。これは、左右からの距離が20で縦横比が1:1、中央配置という制約が忠実に守られているからだが、どうしたら良いだろう? (Quizは時間が余っていれば各自に考えさせたかったスライドだが、時間的にかなり厳しいのでさっさと次に進む)
  • #20 Size Inspectorを使うと、設定した制約を編集できる。ここでは、例えば距離を固定ではなく不等号にしたり、制約に対して優先順位を設定する事ができる。これを使って、先ほどの問題を解決していく。
  • #21 まず、上下左右からの距離を20「以上」にする。 これだけだと、ビューは幅0、高さ0になってしまうので、優先順位を下げて幅を1000にする制約を追加する。これによって、実際に幅が1000にならなくても、限りなく1000に近づくように盤面が大きくなる。 シミュレーターで確認してみよう。
  • #22 次に実際にコードを書いていく前に、Swiftについて軽く勉強する。 「swift tutorial」でグーグルで検索しよう。30分ほどで変数、メソッド、配列、制御文などをおさらいする。他の言語を使った事がある人がターゲットなのでサラッとで良い。 配列まわり(for in、map、filterなど)は割と使うのでここで説明しておいたほうが良いか。
  • #23 iOSやAndroidなど、GUIのアプリは基本的にイベントドリブンでコードを書いていく。すなわち、ユーザーによるタップや、端末の位置情報が更新されたりといったイベントに対して、何をするのかコードを書いていく。
  • #24 最も基本的でかつ実践的なアプリの設計は、アプリをMVCと呼ばれる構成にすることだ。 MVCではアプリをModel - View - Controllerの3つに分離する。Modelはアプリのロジックを担当する。2048では盤面のスライドや、ゲームオーバーの判定などがここにあたる。Viewはその名の通り、ユーザーに見た目を表示するための処理を行う。Controllerは基本的に一画面につき1つ存在し、ユーザーのタップといった入力を受け取り、モデルに処理を依頼して、結果をビューに伝える役割を果たす。ModelとViewの中間に位置する。
  • #25 MVCに従ってプロジェクトをグループ分けする。
  • #26 ゲーム画面のコントローラを作成する。
  • #27 このコントローラはboardViewを持つ。IBOutletというキーワードによって、ストーリーボード上のボードとつなげられるようになる。
  • #28 ストーリーボードで作成したゲーム画面にコントローラを割り当てる。
  • #29 boardViewの関連付けを行う
  • #30 次に数字を表示するTileLabelを作成する。ビュー。
  • #31 TileLabelは、どこのタイルなのかという情報を持つpositionを持つ。
  • #32 タイルラベルを画面に配置する前に、ViewControllerのライフサイクルについて考える。ViewControllerは生成されてから消滅するまで、このようなイベントがそれぞれ発生する。 タイルラベルを配置するのは、オートレイアウトによって盤面のサイズが決定した後でなければいけないので、ViewDidLayoutSubviewsで行う。
  • #33 試しにタイルを1つ配置してみる。
  • #34 シミュレーターで実行して1つタイルが置かれているのを確認できる。 これを16個、しきつめて配置するにはどうすれば良いだろう? (おそらく時間がないので次へ進む)
  • #35 for文を使うとこのように実装できる。
  • #36 いよいよ本日の最難関、ゲームアルゴリズムを作っていく。コーディングの能力だけでなく、どのようにゲームの動作を実装するか頭を働かせなければ作る事ができない。難しく感じたら後ほどゆっくり復習してもらうことをお勧めする。
  • #37 ゲームモデルを作る。
  • #38 このモデルのインスタンスは、このアプリ全体を通して1つだけで良い。このような場合は、シングルトンパターンを用いる。 sharedInstanceとしてstaticな変数で初期化したインスタンスを保持する。以後、Gameのインスタンスは全てGame.sharedInstanceを呼び出すことによって取得する。 ゲームには盤面のサイズを持たせる。ここの数字を4から5にかえたら、5x5の2048をプレイできるように。また、どこのタイルが何の数字なのかわかるように、盤面の状態を多次元配列によって保持する。この配列は初期化の際、全て0で初期化しておこう。 turnという変数に「dynamic」というキーワードがついているのが気になった人は鋭い。これについては後ほど説明する。
  • #39 ゲームモデルには次のような処理ができる必要がある。 まず、ランダムな空白に、2か4を生成する処理。この処理のために、盤面のどこが空白なのか取得する処理も必要だ。 それから、ボードをスライドさせる処理。ボードのスライドは、あるラインのスライドの繰り返しだから、先に1つのラインをスライドさせる処理を作る。 最後に、ゲームが終了したかどうか判定するメソッドを作る。
  • #40 まず盤面の空白のタイルの位置を全て取得するメソッドを作る。これは非常にシンプルで、全てのタイルをループで調べて、0だったら結果に追加すれば良い。 結果はxとyがわかるよう、タプルの配列で返す。
  • #41 次にランダムな位置に2か4を生成するためのメソッドを作る。乱数の生成にはarc4random_uniformという関数を使う。空白の位置の中のどこかに2か4をセットする。 ところで、「2か4」というのも乱数だが、これは「0か1」に1を足して2倍することで得られる。 ここで、次のターンに進むメソッドも一緒に作っておく。2か4を生成して、ターンを1増やすだけだ。
  • #42 さぁ、最も頭を使うのが、スライドの機能だ。 例えば、左図の状態で左にスワイプした際、右図のようになってほしい。 これをどのように実装できるだろうか?
  • #43 これは、3つの手順によって実装できる。まず、0を取り除く。次に同じ数字が隣り合っていたら合体させる。最後に、余った部分を0で埋める。 これらそれぞれについてメソッドを作り、これらのメソッドを呼ぶSlideNumbersというメソッドを作っていこう。
  • #44 まず0を取り除く。これは配列のfilterというメソッドを使うと簡単にできる。 numが0じゃない要素だけを抽出した新しい配列を作って返り値として渡している。
  • #45 次に、隣り合わせの数字が同じだったら合体させる処理。 これは少しだけ複雑になるが、落ち着いて考えればそんなに難しくはない。
  • #46 最後に、空白要素を0で埋める。これは空白要素の数だけ0の要素を持つ配列を新しく作り合体させれば良いだけだ。
  • #47 次に、これら3つのメソッドを呼び出すメソッドを作る。 このメソッドはreverseという引数を受け取る。 というのも、上や左にスワイプするときには、作った3つのメソッドをそのまま呼び出せば良いが、下や右にスワイプするときには、配列を2回反転しないといけないからだ。 (例を参照)
  • #48 これで盤面の1つのラインをスライドさせるメソッドはできた。次に盤面全体をスライドさせるメソッドを作る。スライドする方角に応じて処理が変わるのでswitchによって分岐をさせている。また、引数としてvirtualを受け取り、これがtrueの時には、実際の盤面は更新しないようにする。これは次のゲームオーバーの判定に使う。
  • #49 ゲームオーバーの判定をするために、まずは現在の盤面でスワイプ可能な方角を全て取得するメソッドを作る。 これは、virtualをtrueにしてslideBoardを呼び出してみて、結果が現在の盤面と変わるようならスワイプ可能とみなすことで実装できる。 スワイプ可能な方角の数が0だったら、ゲームオーバーだ。
  • #50 ゲームアルゴリズムの山場を越えたので、最後に実際にユーザーのスワイプイベントを受け取ってModelの処理を実行し、ビューを更新する。
  • #51 盤面のサイズを固定で4としているところを、GameインスタンスのboardSizeで置き換える。 また、描画が終わった後に、最初のターンを開始するよう、viewDidLayoutSubviewsに1行追加する。
  • #52 スワイプはUISwipeGestureRecognizerを使うことでハンドリングできる。 上下左右それぞれに対して、スワイプされたらswipedメソッドを呼び出すように設定する。 swipedでは、スワイプ可能な方角であれば盤面をスライドさせ、また、ゲームオーバーになったらGame Over.とログに出すようにしている。
  • #53 これで動作は完成したが、まだ結果がビューに反映されない。 ビューの更新にはいくつか方法があるが、ここではKVOというテクニックを紹介したい。 KVOを使うと、あるインスタンスの、dynamicキーワードがついた変数を別のインスタンスから監視し、その変数の値が変わった時にイベントを受け取れるようになる。 今回は、TileLabelにGameのturnを監視させ、次のターンになったときにイベントを受け取るようにする。
  • #54 タイルラベルの初期化の後にaddObserverをしてKVOを設定する。これによって、ターンが変わった際にobserveValueForKeyPathメソッドが呼び出されるようになる。 addObserverをした際は、必ずdeinitの中でremoveObserverをすること。監視を解除しないとタイルラベルが消滅できなくなり、メモリの問題が発生する。
  • #55 イベントを受け取ったら、ラベルのテキストと背景色を変える。背景色に関して簡単に実装する場合、numに応じてif文で色をセットしていく方法で良いが、長くなる。 ここでは色相(Hue)を使ってタイルの色を計算している。色相についてや、再帰的なメソッドについて説明すると長くなるので、ここでは詳しく説明しない。
  • #56 完成したアプリでデモを見せる。