[cocos2d-x] 2048 게임 만들기 feat. SpriteBuilder and Swift

     



퍼즐게임중에 2048이란 게임이 있는데 이 게임을 한번 따라서 만들어 보겠습니다. 

추가적인 이미지나 Resource를 사용하지 않고 아주 노멀하게 ~ 기본적인 소스들만 사용해서 만들 예정입니다.


일단 2048이라는 게임을 모르시는 분들도 있기때문에 아래 링크를...


http://2048game.com/



몇 번 플레이해보시면 아시겠지만 2의 배수로 계속 더해 2048을 만드는 것이 목적인 게임입니다. 한 때 모바일로 출시되 많은 사랑을 받은 게임인데요, 사실 2048보다는 Three라는 게임이 원조입니다. 그 뒤에 2048이 나왔는데 어찌 마케팅을 2048이 더 잘 해서 2048이 더 유명해졌네요.

암튼 Three보다는 2048이 게임 로직이 더 쉽기 때문에 2048을 따라해보려고 합니다.


개발 환경은 SpriteBuilder와  Swift 언어를 사용하겠습니다. 


일단 SpriteBuilder에서 신규 프로젝트를 만듭니다.



개발 언어가 obj-C가 아니라 Swift입니다!!!


디폴트 프로젝트가 만들어지고 나면 정상적으로 실행되는지 x-code로 publishing 한 번 해봅시다.



X-code에서 실행시키는 방법은 간단합니다. SpriteBuilder로 만든 프로젝터 경로로 가서 .xcodeproj 파일을 실행하면 됩니다.



처음 프로그램을 실행시키면 swift언어를 최신버전으로 convert할 것인지 물어봅니다. 뭐 항상 Yes를 누르긴 하는데 항상 바꿀게 없다고 나옵니다. X-code에서 가상머신을 iphone6로 설정 후 실행하면 아래와 같이 실행될 겁니다.



SpriteBuilder의 기본 화면입니다. 자 이제 이 화면을 2048 화면으로 바꿔볼까요?



먼저 2048은 세로화면에서 진행되기 때문에 화면을 세로로 고정해 줄 필요가 있습니다. SpriteBuilder에서  File -> Proejct setting에서 Global 옵션에 있는 Orientation을 Portrait으로 바꿔줍니다. 그리고 MainScene.ccb에 있는 기본문구 SpriteBuilder라고 써있는 CCLabelTTF를 지워줍니다.



게임화면은 아주 간단합니다. 현재 점수를 나타내는 Score와 최고점수를 나타내는 HighScore, 그리고 실제 타일들이 움직이는 Grid만 있으면 됩니다.



글씨들은 Label TTF를 사용해서 아~~무 위치나 적당히 하면 됩니다. Grid는 anchorpoint를 0.5, 0.5를 주고 화면 중앙에 위치, 300x300크기의 Color Node로 만들었습니다. 이미지셋을 안쓰기 때문에 색을 주기 위해 ColorNode를 씁시다!!

X-code에서 사용하기 위해 Grid는 커스텀클래스로 만들고, Label은 변수 이름을 지정합시다. (점수, 최고점수 레이블은 필요 없고, 숫자 부분만 각각 scoreLabel, highscoreLabel로 지정!)




이제 실제로 숫자가 적혀나올 Tile을 만듭니다.



크기는 기본적으로 70x70을 주었습니다. Grid가 300x300이기 때문에 가로 세로 4개씩 들어가려면 4*70 = 280 이고 여백도 조금 필요해서 70이 적당합니다. 기본 노드의 크기를 70x70으로 만들고 그 아래 컬러노드를 추가합니다.



컬러노드는 대충 색을 아무거나 지정하고 상위 노드와 같은 크기, 같은위치에 위치하게 설정합니다. 타일에 표기할 Label도 넣습니다.



자 이제 부모노드인 CCNode는 Tile의 커스텀클래스를 만들고, Color노드는 backgroundNode, Label은 valueNode라고 이름을 지정해줍니다.



이 부분을 잘 하셔야지 publish했을 때 Xcode에서 실행이 잘됩니당. 헷갈리시면 안됩니다!!


이젠 Xcode가서 클래스와 변수를 선언하고 실행해 봅시다.


1
2
3
4
5
6
7
8
9
10
import Foundation
 
class MainScene: CCNode {
 
    weak var scoreLabel: CCLabelTTF!
    weak var highscoreLabel: CCLabelTTF!
    weak var grid: Grid!
    
}
 
cs


메인의 코드입니다. 


1
2
3
4
5
import Foundation
 
class Grid: CCNodeColor {
 
}
cs


Grid는 커스텀클래스이기 때문에 swift파일을 생성해주셔야됩니다. CCNode가 아니라 CCNodeColor인점 확인해주세요~

1
2
3
4
5
6
7
8
import Foundation
 
class Tile: CCNode {
 
    weak var backgroundNode: CCNodeColor!
    weak var valueLabel: CCLabelTTF!
    
}
cs


Tile 역시 swift파일을 생성해주시고 backgroundNode와 valueNode를 불러옵니다. 

선언을 다 하고 프로그램을 실행하면 아래와 같은 화면이 뜹니다.


잘 실행이 된다면 선언은 잘된겁니다. 만약 실행이 안되면 이름이 틀렸거나 커스텀클래스를 선언하지 않았거나 하는 문제가 있다는 것 이므로 다시 한번 잘 살펴봅시다.


가장 먼저 할 것은 그리드에 배경을 깔아주는 것입니다. 4x4의 그림자 타일을 깔아서 타일의 위치가 어디어디인지 틀을 잡아주는 역할을 합니다. 또한 타일의 크기나 여백의 크기, 그리드 사이즈 같은 기본 변수들을 선언해줍니다.


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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import Foundation
 
class Grid: CCNodeColor {
 
    let gridSize = 4 // 그리드 사이즈
    
    var columnWidth: CGFloat = 0 // 타일의 가로 크기
    var columnHeight: CGFloat = 0 // 타일의 세로 크기
    var tileMarginVertical: CGFloat = 0 // 타일 세로 여백 크기
    var tileMarginHorizontal: CGFloat = 0 // 타일 가로 여백 크기
    
    // 클래스가 실행될 때 자동으로 실행됨
    func didLoadFromCCB(){
        setupBackground()
    }
    
    // 그리드에 배경을 깔아줌
    func setupBackground() {
        
        // 타일 클래스로 Tile 노드를 읽어옴
        let tile = CCBReader.load("Tile") as! Tile
        
        // 타일의 가로 세로 크기 설정
        columnWidth = tile.contentSize.width
        columnHeight = tile.contentSize.height
        
        // 여백 크기를 어떻게 할지 계산
        tileMarginHorizontal = (contentSize.width - (CGFloat(gridSize) * columnWidth)) / CGFloat(gridSize + 1)
        tileMarginVertical = (contentSize.height - (CGFloat(gridSize) * columnHeight)) / CGFloat(gridSize + 1)
        
        var x = tileMarginHorizontal
        var y = tileMarginVertical
        
        // 배경으로 쓸 타일을 4x4그리드에 한장씩 배치
        for _ in 0..<gridSize{
            x = tileMarginHorizontal
            for _ in 0..<gridSize{
                let backgroundTile = CCNodeColor(color: CCColor.grayColor())
                backgroundTile.contentSize = CGSize(width: columnWidth, height: columnHeight)
                backgroundTile.position = CGPoint(x: x, y: y)
                addChild(backgroundTile)
                x += columnWidth + tileMarginHorizontal
                
            }
            y += columnHeight + tileMarginVertical
        }
        
    }
    
    
}
cs


자세한 설명은 주석을 참고하세요~! 실행하면 아래와 같습니다.


기본적인 틀이 완성되었습니다. 이제 실제로 움직이는 타일을 생성하는 소스를 추가해보겠습니다.

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
//
//  Grid.swift
//  cholol2048
//
//  Created by Cho on 2016. 6. 5..
//  Copyright © 2016년 Apportable. All rights reserved.
//
 
import Foundation
 
class Grid: CCNodeColor {
 
    let gridSize = 4 // 그리드 사이즈
    
    var columnWidth: CGFloat = 0 // 타일의 가로 크기
    var columnHeight: CGFloat = 0 // 타일의 세로 크기
    var tileMarginVertical: CGFloat = 0 // 타일 세로 여백 크기
    var tileMarginHorizontal: CGFloat = 0 // 타일 가로 여백 크기
    
    // 시작 타일수
    let startTiles = 2
    
    
    // 그리드 배열 관리
    var gridArray = [[Tile?]]()
    var noTile: Tile? = nil
    
    // 클래스가 실행될 때 자동으로 실행됨
    func didLoadFromCCB(){
        setupBackground()
        
        // 그리드에 타일들을 초기화
        for _ in 0..<gridSize {
            var column = [Tile?]()
            for _ in 0..<gridSize {
                column.append(noTile)
            }
            gridArray.append(column)
        }
        
        // 시작타일 생성
        spawnStartTiles()
        
    }
    
    // 그리드에 배경을 깔아줌
    func setupBackground() {
        
        // 타일 클래스로 Tile 노드를 읽어옴
        let tile = CCBReader.load("Tile") as! Tile
        
        // 타일의 가로 세로 크기 설정
        columnWidth = tile.contentSize.width
        columnHeight = tile.contentSize.height
        
        // 여백 크기를 어떻게 할지 계산
        tileMarginHorizontal = (contentSize.width - (CGFloat(gridSize) * columnWidth)) / CGFloat(gridSize + 1)
        tileMarginVertical = (contentSize.height - (CGFloat(gridSize) * columnHeight)) / CGFloat(gridSize + 1)
        
        var x = tileMarginHorizontal
        var y = tileMarginVertical
        
        // 배경으로 쓸 타일을 4x4그리드에 한장씩 배치
        for _ in 0..<gridSize{
            x = tileMarginHorizontal
            for _ in 0..<gridSize{
                let backgroundTile = CCNodeColor(color: CCColor.grayColor())
                backgroundTile.contentSize = CGSize(width: columnWidth, height: columnHeight)
                backgroundTile.position = CGPoint(x: x, y: y)
                addChild(backgroundTile)
                x += columnWidth + tileMarginHorizontal
                
            }
            y += columnHeight + tileMarginVertical
        }
        
    }
    
    // 배열 인덱스를 이용해 tile 위치를 반환하는 함수
    func positionForColumn(column: Int, row: Int) -> CGPoint {
        let x = tileMarginHorizontal + CGFloat(column) * (tileMarginHorizontal + columnWidth)
        let y = tileMarginVertical + CGFloat(row) * (tileMarginVertical + columnHeight)
        return CGPoint(x: x, y: y)
    }
    
    // 특정 위치에 tile 생성
    func addTileAtColumn(column: Int, row: Int) {
        let tile = CCBReader.load("Tile") as! Tile
        gridArray[column][row] = tile
        tile.scale = 0
        addChild(tile)
        tile.anchorPoint = ccp(0,0// 없어도 되는데 SpriteBuilder에서 anchorPoint를 잘못지정해서 넣음.. ㅎ
        tile.position = positionForColumn(column, row: row)
        let delay = CCActionDelay(duration: 0.3)
        let scaleUp = CCActionScaleTo(duration: 0.2, scale: 1)
        let sequence = CCActionSequence(array: [delay, scaleUp])
        tile.runAction(sequence)
    }
    
    // 랜덤한 위치에 Tile 생성
    func spawnRandomTile() {
        var spawned = false
        while !spawned {
            let randomRow = Int(CCRANDOM_0_1() * Float(gridSize))
            let randomColumn = Int(CCRANDOM_0_1() * Float(gridSize))
            let positionFree = gridArray[randomColumn][randomRow] == noTile
            if positionFree {
                addTileAtColumn(randomColumn, row: randomRow)
                spawned = true
            }
        }
    }
    
    // 시작할 때 생성할 타일 수만큼 타일 생성
    func spawnStartTiles(){
        for _ in 0..<startTiles {
            spawnRandomTile()
        }
    }
    
    
}
cs


역시 소스 설명은 주석으로... 실행하면 아래와 같습니다.



타일이 생성되었긴 한데 숫자가 0이네요. Tile.swift에서 숫자를 넣어봅시다.

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
26
27
28
29
30
31
32
//
//  Tile.swift
//  cholol2048
//
//  Created by Cho on 2016. 6. 5..
//  Copyright © 2016년 Apportable. All rights reserved.
//
 
import Foundation
 
class Tile: CCNode {
 
    weak var backgroundNode: CCNodeColor!
    weak var valueLabel: CCLabelTTF!  // SpriteBuilder에서 valueNode로 했었는데 valueLabel로 수정하세요~!
    
    
    // value 초기값 0으로 지정.
    var value: Int = 0 {
        // 값이 변경될 때마다 자동으로 실행
        didSet{
            valueLabel.string = "\(value)"
        }
    }
    
    
    func didLoadFromCCB() {
        // 타일이 생성될 때마다 2 or 4 값으로 설정
        value = Int(CCRANDOM_MINUS1_1() + 2* 2
    }
    
}
 
cs


위와같이 바꿔주고 실행하면 다음과 같습니다. (오류가 나시는 분은 valueLabel 변경 주석을 참고하세요.)


자 이제 이 다음에 타일을 움직이는 것과 같은 숫자일 경우 합치는것, 점수 카운트 등이 남았는데..
따로 설명할 수가 없어서 갑작스럽게 풀코드로 가겠습니다. 


그 전에 일단 게임오버 화면부터 만들어보겠습니다.



뭐 대충 위와같이 GameEnd라는 이름의 ccb를 만듭니다 (노드로 만듭니다.)

루트의 크기를 300x200으로 해주시고 컬러노드 넣어서 색 넣어주시고 레이블과 버튼을 위치시킵니다.  위치는 뭐 그렇게 중요하지 않습니다. 중요한건 버튼에 콜백메소드 설정과 Label 이름 설정, 커스텀 클래스 이름 설정입니다.



빼먹지 말고 하나하나 지정해줍시다.

자 이제 필요한 화면은 다 만들었고, 아래 코드까지 전부 입력하면 완성입니다.

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
//
//  Grid.swift
//  cholol2048
//
//  Created by Cho on 2016. 6. 5..
//  Copyright © 2016년 Apportable. All rights reserved.
//
 
import Foundation
 
class Grid: CCNodeColor {
 
    // 게임 점수
    var score: Int = 0 {
        didSet {
            var mainScene = parent as! MainScene
            mainScene.scoreLabel.string = "\(score)"
        }
    }
 
    // 승리 점수
    let winTile = 2048
    
    let gridSize = 4 // 그리드 사이즈
    
    var columnWidth: CGFloat = 0 // 타일의 가로 크기
    var columnHeight: CGFloat = 0 // 타일의 세로 크기
    var tileMarginVertical: CGFloat = 0 // 타일 세로 여백 크기
    var tileMarginHorizontal: CGFloat = 0 // 타일 가로 여백 크기
    
    // 시작 타일수
    let startTiles = 2
    
    
    // 그리드 배열 관리
    var gridArray = [[Tile?]]()
    var noTile: Tile? = nil
    
    // 클래스가 실행될 때 자동으로 실행됨
    func didLoadFromCCB(){
        
        //초기화
        setupBackground()
        setupGestures()
        
        // 그리드에 타일들을 초기화
        for _ in 0..<gridSize {
            var column = [Tile?]()
            for _ in 0..<gridSize {
                column.append(noTile)
            }
            gridArray.append(column)
        }
        
        // 시작타일 생성
        spawnStartTiles()
        
    }
    
    // 그리드에 배경을 깔아줌
    func setupBackground() {
        
        // 타일 클래스로 Tile 노드를 읽어옴
        let tile = CCBReader.load("Tile") as! Tile
        
        // 타일의 가로 세로 크기 설정
        columnWidth = tile.contentSize.width
        columnHeight = tile.contentSize.height
        
        // 여백 크기를 어떻게 할지 계산
        tileMarginHorizontal = (contentSize.width - (CGFloat(gridSize) * columnWidth)) / CGFloat(gridSize + 1)
        tileMarginVertical = (contentSize.height - (CGFloat(gridSize) * columnHeight)) / CGFloat(gridSize + 1)
        
        var x = tileMarginHorizontal
        var y = tileMarginVertical
        
        // 배경으로 쓸 타일을 4x4그리드에 한장씩 배치
        for _ in 0..<gridSize{
            x = tileMarginHorizontal
            for _ in 0..<gridSize{
                let backgroundTile = CCNodeColor(color: CCColor.grayColor())
                backgroundTile.contentSize = CGSize(width: columnWidth, height: columnHeight)
                backgroundTile.position = CGPoint(x: x, y: y)
                addChild(backgroundTile)
                x += columnWidth + tileMarginHorizontal
                
            }
            y += columnHeight + tileMarginVertical
        }
        
    }
    
    // 배열 인덱스를 이용해 tile 위치를 반환하는 함수
    func positionForColumn(column: Int, row: Int) -> CGPoint {
        let x = tileMarginHorizontal + CGFloat(column) * (tileMarginHorizontal + columnWidth)
        let y = tileMarginVertical + CGFloat(row) * (tileMarginVertical + columnHeight)
        return CGPoint(x: x, y: y)
    }
    
    // 특정 위치에 tile 생성
    func addTileAtColumn(column: Int, row: Int) {
        let tile = CCBReader.load("Tile") as! Tile
        gridArray[column][row] = tile
        tile.scale = 0
        addChild(tile)
        tile.anchorPoint = ccp(0,0// 없어도 되는데 SpriteBuilder에서 anchorPoint를 잘못지정해서 넣음.. ㅎ
        tile.position = positionForColumn(column, row: row)
        let delay = CCActionDelay(duration: 0.3)
        let scaleUp = CCActionScaleTo(duration: 0.2, scale: 1)
        let sequence = CCActionSequence(array: [delay, scaleUp])
        tile.runAction(sequence)
    }
    
    // 랜덤한 위치에 Tile 생성
    func spawnRandomTile() {
        var spawned = false
        while !spawned {
            let randomRow = Int(CCRANDOM_0_1() * Float(gridSize))
            let randomColumn = Int(CCRANDOM_0_1() * Float(gridSize))
            let positionFree = gridArray[randomColumn][randomRow] == noTile
            if positionFree {
                addTileAtColumn(randomColumn, row: randomRow)
                spawned = true
            }
        }
    }
    
    // 시작할 때 생성할 타일 수만큼 타일 생성
    func spawnStartTiles(){
        for _ in 0..<startTiles {
            spawnRandomTile()
        }
    }
    
    
    // 제스처 정의
    func setupGestures() {
        
        let swipeLeft = UISwipeGestureRecognizer(target: self, action: #selector(Grid.swipeLeft))
        swipeLeft.direction = .Left
        CCDirector.sharedDirector().view.addGestureRecognizer(swipeLeft)
        let swipeRight = UISwipeGestureRecognizer(target: self, action: #selector(Grid.swipeRight))
        swipeRight.direction = .Right
        CCDirector.sharedDirector().view.addGestureRecognizer(swipeRight)
        let swipeUp = UISwipeGestureRecognizer(target: self, action: #selector(Grid.swipeUp))
        swipeUp.direction = .Up
        CCDirector.sharedDirector().view.addGestureRecognizer(swipeUp)
        let swipeDown = UISwipeGestureRecognizer(target: self, action: #selector(Grid.swipeDown))
        swipeDown.direction = .Down
        CCDirector.sharedDirector().view.addGestureRecognizer(swipeDown)
        
    }
    
    // 제스처 왼쪽
    func swipeLeft(){
        print("왼쪽")
        move(CGPoint(x: -1, y: 0))
        
    }
    // 제스처 오른쪽
    func swipeRight(){
        print("오른쪽")
        move(CGPoint(x: 1, y: 0))
        
    }
    
    // 제스처 위
    func swipeUp(){
        print("위")
        move(CGPoint(x: 0, y: 1))
        
    }
    
    // 제스처 아래
    func swipeDown(){
        print("아래")
        move(CGPoint(x: 0, y: -1))
        
    }
    
    
    // 제스처를 취할 경우 타일이 움직이는 함수
    func move(direction: CGPoint){
        var currentX = 0
        var currentY = 0
        
        // 타일이 실제로 움직였는지 판단. 못움직이는 방향으로 움직였을 때 라운드가 진행 안되도록 하기 위해..
        var movedTilesThisRound = false
        
        /* 제스처의 방향을 판단하여 그리드 전체 타일을 검사할 때 시작점을 정의
           게임로직의 가장 핵심이라고 할 수 있다. 만약 이동방향이 오른쪽이라면 
           제일 오른쪽 아래 타일부터 세로로 확인. 이동방향이 왼쪽이라면 왼쪽아래 타일부터 세로로 확인.
           이동방향이 아래면 왼쪽아래 타일부터 가로로 확인. 이동방향이 위면 왼쪽 위 타일부터 가로로 확인.
        */
        while indexValid(currentX, y: currentY) {
            let newX = currentX + Int(direction.x)
            let newY = currentY + Int(direction.y)
            
            if indexValid(newX, y: newY) {
                currentX = newX
                currentY = newY
            } else {
                break;
            }
        }
        
        var initialY = currentY
        
        var xChange = Int(-direction.x)
        var yChange = Int(-direction.y)
        
        if xChange == 0 {
            xChange = 1
        }
        
        if yChange == 0 {
            yChange = 1
        }
        
        // 실제로 타일을 이동시키는 부분
        while indexValid(currentX, y: currentY) {
            while indexValid(currentX, y: currentY) {
                if var tile = gridArray[currentX][currentY] {
                    var newX = currentX
                    var newY = currentY
                    while indexValidAndUnoccupied(newX + Int(direction.x), y: newY + Int(direction.y)){
                        newX += Int(direction.x)
                        newY += Int(direction.y)
                    }
                    
                    var performMove = false
                    if indexValid(newX + Int(direction.x), y: newY + Int(direction.y)){
                        var otherTileX = newX + Int(direction.x)
                        var otherTileY = newY + Int(direction.y)
                        
                        if let otherTile = gridArray[otherTileX][otherTileY]{
                            // 이동하는 타일에 이동선상에 있는 타일이 같은 값이고, 이번 라운드에 합처진 타일이 아닌지 체크
                            if tile.value == otherTile.value && !otherTile.mergedThisRound{
                                mergeTilesAtindex(currentX, y: currentY, withTileAtIndex: otherTileX, y: otherTileY)
                                movedTilesThisRound = true
                            } else {
                                performMove = true
                            }
                        }
                    } else {
                        performMove = true
                    }
                    
                    if performMove{
                        if newX != currentX || newY != currentY {
                            moveTile(tile, fromX: currentX, fromY: currentY, toX: newX, toY: newY)
                            movedTilesThisRound = true
                        }
                    }
                    
                    
                }
                currentY += yChange
            }
            currentX += xChange
            currentY = initialY
        }
        
        // 이번 라운드에 타일이 움직였으면 다음 라운드 진행
        if movedTilesThisRound {
            nextRound()
        }
    }
    
    
    // 인덱스가 그리드 안에 있는지 체크. (0, 0) ~ (3, 3) 범위 내에 있는지 체크
    func indexValid(x: Int, y: Int) -> Bool {
        var indexValid = true
        indexValid = (x >= 0 ) && (y >= 0 )
        if indexValid {
            indexValid = x < Int(gridArray.count)
            if indexValid {
                indexValid = y < Int(gridArray[x].count)
            }
        }
        
        return indexValid
    }
    
    // 적합한 인덱스 이면서 해당 인덱스에 타일이 있는지 체크
    func indexValidAndUnoccupied(x: Int, y: Int) -> Bool {
        let indexValid = self.indexValid(x, y: y)
        if !indexValid {
            return false
        }
        
        // unoccupied?
        return gridArray[x][y] == noTile
    }
    
    
    
    // 타일 이동 애니메이션 및 배열값 설정
    func moveTile(tile: Tile, fromX: Int, fromY: Int, toX: Int, toY: Int){
        gridArray[toX][toY] = gridArray[fromX][fromY]
        gridArray[fromX][fromY] = noTile
        let newPosition = positionForColumn(toX, row: toY)
        let moveTo = CCActionMoveTo(duration: 0.2, position: newPosition)
        tile.runAction(moveTo)
    }
    
    // 타일이 합처질경우 애니메이션 및 값 설정
    func mergeTilesAtindex(x: Int, y: Int, withTileAtIndex otherX: Int, y otherY: Int){
        let mergedTile = gridArray[x][y]!
        let otherTile = gridArray[otherX][otherY]!
        score += mergedTile.value + otherTile.value
        gridArray[x][y] = noTile
        
        var otherTilePostion = positionForColumn(otherX, row: otherY)
        let moveTo = CCActionMoveTo(duration: 0.2, position: otherTilePostion)
        let remove = CCActionRemove()
        let mergeTile = CCActionCallBlock(block: { () -> Void in
            otherTile.value *= 2
        })
        var checkWin = CCActionCallBlock(block: { () -> Void in
            if otherTile.value == self.winTile {self.win()}
        })
        
        let sequence = CCActionSequence(array: [moveTo, mergeTile, checkWin, remove])
        mergedTile.runAction(sequence)
        otherTile.mergedThisRound = true
    }
    
    // 다음라운드 메소드
    func nextRound() {
        
        // 랜덤으로 하나의 타일 생성
        spawnRandomTile()
        
        // 모든 타일 합처젔는지 체크 변수 초기화
        for column in gridArray {
            for tile in column {
                tile?.mergedThisRound = false
            }
        }
        
        // 게임오버 여부 체크.
        if !movePossible(){
            lose()
        }
    }
    
    // 현재 상태에서 이동이 더 가능한지 체크. 전체 타일에 대해 이웃한 같은값을 가진 타일이 있는지 체크
    func movePossible() -> Bool {
        for i in 0..<gridSize {
            for j in 0..<gridSize {
                if let tile = gridArray[i][j]{
                    let topNeighbor = tileForIndex(i, y: j+1)
                    let bottomNeightbor = tileForIndex(i, y: j-1)
                    let leftNeighbor = tileForIndex(i-1, y: j)
                    let rightNeighbor = tileForIndex(i+1, y: j)
                    let neighbors = [topNeighbor, bottomNeightbor, leftNeighbor, rightNeighbor]
                    
                    for neigbor in neighbors {
                        if let neighborTile = neigbor{
                            if neighborTile.value == tile.value{
                                return true
                            }
                        }
                    }
                } else {
                    return true
                }
            }
        }
        
        return false
    }
    
    // 인덱스에 타일이 있는지 여부
    func tileForIndex(x: Int, y: Int) -> Tile? {
        return indexValid(x, y: y) ? gridArray[x][y] : noTile
    }
    
    // 게임오버
    func lose(){
        endGameWithMessage("You Lose")
    }
    
    // 게임승리
    func win(){
        endGameWithMessage("You Win!")
    }
    
    // 게임오버 창 띄우기.
    func endGameWithMessage(message: String) {
        let defaults = NSUserDefaults.standardUserDefaults()
        let highscore = defaults.integerForKey("highscore")
        if score > highscore {
            defaults.setInteger(score, forKey: "highscore")
            defaults.synchronize()
        }
        
        let gameEndPopover = CCBReader.load("GameEnd") as! GameEnd
        gameEndPopover.positionType = CCPositionType(xUnit: .Normalized, yUnit: .Normalized, corner: .BottomLeft)
        gameEndPopover.position = ccp( 0.50.5 )
        gameEndPopover.zOrder = Int.max
        gameEndPopover.setMessage(message, score: score)
        addChild(gameEndPopover)
    }
 
    
}
cs


거의 메인이라고 할 수 있는 Grid입니다. 생각보다 타일 움직이는 로직이 복잡합니다. 주석을 잘 읽으시고 코드를 한번씩 따라가 보셔야지 이해 할 수 있습니다.

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
26
27
28
29
30
31
import Foundation
 
class MainScene: CCNode {
 
    weak var scoreLabel: CCLabelTTF!
    weak var highscoreLabel: CCLabelTTF!
    weak var grid: Grid!
    
    
    // 최고점수를 체크하기위한 메소드. 유저 공통 저장공간에 저장한다.
    func updateHighscore() {
        let newHighscore = NSUserDefaults.standardUserDefaults().integerForKey("highscore")
        highscoreLabel.string = "\(newHighscore)"
    }
    
    func didLoadFromCCB() {
    
        NSUserDefaults.standardUserDefaults().addObserver(self, forKeyPath: "highscore", options: [], context: nil)
        // 시작시 최고점수를 공통변수에서 꺼내온다.
        updateHighscore()
    }
    
        // 최고점수 값이 없데이트 되면 변경해준다.
    override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
        if keyPath == "highscore" {
            updateHighscore()
        }
    }
    
}
 
cs


메인.


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
26
27
//
//  GameEnd.swift
//  cholol2048
//
//  Created by Cho on 2016. 6. 5..
//  Copyright © 2016년 Apportable. All rights reserved.
//
 
import Foundation
 
class GameEnd: CCNode{
    
    weak var messageLabel: CCLabelTTF!
    weak var scoreLabel: CCLabelTTF!
    
    // 재시작 콜백 메소드
    func restart(){
        let mainScene = CCBReader.loadAsScene("MainScene")
        CCDirector.sharedDirector().presentScene(mainScene)
    }
    
    // 팝업에 메시지 띄우기
    func setMessage(message: String, score: Int){
        messageLabel.string = message
        scoreLabel.string = "\(score)"
    }
}
cs


게임 오버.


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
26
27
28
29
30
31
32
33
34
//
//  Tile.swift
//  cholol2048
//
//  Created by Cho on 2016. 6. 5..
//  Copyright © 2016년 Apportable. All rights reserved.
//
 
import Foundation
 
class Tile: CCNode {
 
    weak var backgroundNode: CCNodeColor!
    weak var valueLabel: CCLabelTTF!  // SpriteBuilder에서 valueNode로 했었는데 valueLabel로 수정하세요~!
    
    // 이번 라운드에 합처진 타일인지 체크
    var mergedThisRound = false
    
    // value 초기값 0으로 지정.
    var value: Int = 0 {
        // 값이 변경될 때마다 자동으로 실행
        didSet{
            valueLabel.string = "\(value)"
        }
    }
    
    
    func didLoadFromCCB() {
        // 타일이 생성될 때마다 2 or 4 값으로 설정
        value = Int(CCRANDOM_MINUS1_1() + 2* 2
    }
    
}
 
cs


타일.


총 4개의 소스가 있습니다. 

완성된 화면은 아래와 같습니다.



퍼즐게임이다 보니 아무래도 로직이 복잡합니다.

병합되는지 체크하는 부분이나 타일이 이동되는 부분은 천천히 시간을 갖고 보시길 바랍니다.~





반응형

댓글

Designed by JB FACTORY