Introduction
게임 엔진에 대한 Metal Rendering 이후, 게임을 작성하는 방법에 대해 알아보자.
Breakout* 게임을 따라 Raybreak라는 프로젝트를 만들어볼 것이다.
Breakout (비디오 게임)은 1976년 아타리가 개발한 아케이드 게임.
간단한 게임이지만 대부분의 게임에서 사용하는 많은 원칙이 포함되어 있다.
- 새로운 Model이 있는 새로운 Scene을 만든다.
- 게임 경기장의 크기를 결정하고 모든 Model의 시작 위치를 계산한다. 그런 다음 Models bounding Box를 사용하여 2D에서 간단한 충돌 검사를 수행한다. (=모든 방향에서 모델의 높이와 너비를 알 수 있다.)
- 효과음과 음악을 게임에 추가하고 게임을 마무리하기 위해 승패 여부를 보여주는 두 번째 Scene을 만든다.
RayBreak: the Game
RayBreak이라는 Breakout 클론을 만들 것이다.
옆으로 움직이는 패들Paddle, 벽돌Bricks, 테두리Border에 대한 Model이 필요하다.
Player가 Key를 누르는 즉시 공이 움직이기 시작한다.
공이 벽돌과 충돌하면 벽돌이 사라진다.
게임이 진행되는 동안 공은 경계선과 패들에서 계속 튀게 된다.
이 게임의 목표는 공을 패들과 함께 유지하고 모든 벽돌을 제거하는 것이다.
Player가 공을 놓치더라도 3번의 기회가 있고, 그 후에도 실패하면 게임에서 지게 된다.
트랙패드나 마우스로 게임 내에서 카메라를 움직이고 왼쪽 및 오른쪽 화살표를 눌러 패들을 옆으로 움직일 수 있다.
x축과 z축에 게임을 배치하고 y 방향은 무시합니다. 모든 모델은 y축에서 0이 된다. (사실상 2D)
나무 테두리 모델은 원점(0, 0, 0)을 중심으로 배치된다.
게임 너비와 높이는 테두리 크기에서 가져온다.
게임의 너비와 높이를 기준으로 벽돌, 공, 노를 어디에 놓을지 계산한다.
공은 물체에 맞으면 튕겨나와야 한다.
즉, 사용하는 Model들에 BoundingBox를 설정하여 충돌을 처리해야 한다.
Model.swift
self.boundingBox = mdlMeshes[0].boundingBox
Node.swift
var boundingBox = MDLAxisAlignedBoundingBox()
// BoundingBox의 Transform이 ModelTransform과 일치하도록
// BoundingBox를 WorldTransform로 반환하는 함수
func worldBoundingBox(matrix: float4x4? = nil) -> Rect {
var worldMatrix = self.worldMatrix
if let matrix = matrix {
worldMatrix = worldMatrix * matrix
}
var lowerLeft = SIMD4<Float>(boundingBox.minBounds.x, 0, boundingBox.minBounds.z, 1)
lowerLeft = worldMatrix * lowerLeft
var upperRight = SIMD4<Float>(boundingBox.maxBounds.x, 0, boundingBox.maxBounds.z, 1)
upperRight = worldMatrix * upperRight
return Rect(x: lowerLeft.x,
z: lowerLeft.z,
width: upperRight.x - lowerLeft.x,
height: upperRight.z - lowerLeft.z)
}
Build the Arena
Model Space에서 각 Model의 원점Origin은 중심에 있다.
패들과 공을 x축의 0에, z의 음수 거리에 배치하여 게임 영역의 앞쪽 절반에 보여지도록 만든다.
벽돌 배치
게임의 구성은 6개의 Row와 4개의 Column으로 이루어진다.
하지만 자유롭게 변경할 수 있어야 하기 때문에 Flexible한 Layout이 필요하다.
가로 : 왼쪽 → 오른쪽으로 배치 (게임 영역의 가장 왼쪽 + Left margin부터 시작)
세로 : 위 → 아래로 배치
시작점을 (r, c)라고 했을 때.
Brick 1 : (r, LeftMargin + c)
Brick 2 : (r, LeftMargin + c + Gap + Width * 1.5)
…
RayBreak.swift
import Foundation
class RayBreak: Scene {
enum Constants {
static let columns = 4
static let rows = 6
static let paddleSpeed: Float = 0.2
static let ballSpeed: Float = 10
}
let paddle = Model(name: "paddle")
let ball = Model(name: "ball")
let border = Model(name: "border")
let bricks = Instance(name: "brick",
instanceCount: Constants.rows * Constants.columns)
var gameArea: (width: Float, height: Float) = (0, 0)
func setupBricks() {
let margin = gameArea.width * 0.1
let brickWidth = bricks.worldBoundingBox().width
let halfGameWidth = gameArea.width / 2
let halfGameHeight = gameArea.height / 2
let halfBrickWidth = brickWidth / 2
let cols = Float(Constants.columns)
let rows = Float(Constants.rows)
let hGap = (gameArea.width - brickWidth * cols - margin * 2) / (cols - 1)
let vGap = (halfGameHeight - brickWidth * rows - margin + halfBrickWidth) / (rows - 1)
for row in 0..<Constants.rows {
for column in 0..<Constants.columns {
let frow = Float(row)
let fcol = Float(column)
var position = SIMD3<Float>(repeating: 0)
position.x = margin + hGap * fcol + brickWidth * fcol + halfBrickWidth
position.x -= halfGameWidth
position.z = vGap * frow + brickWidth * frow
let transform = Transform(position: position, rotation: SIMD3<Float>(repeating: 0), scale: 1)
bricks.transforms[row * Constants.columns + column] = transform
}
}
}
override func setupScene() {
camera.rotation = [-0.78, 0, 0]
camera.distance = 13.5
camera.target.y = -2
gameArea.width = border.worldBoundingBox().width - 1
gameArea.height = border.worldBoundingBox().height - 1
add(node: paddle)
add(node: ball)
add(node: border)
add(node: bricks)
paddle.position.z = -border.worldBoundingBox().height / 2.0 + 2
ball.position.z = paddle.position.z + 1
setupBricks()
}
override func updateScene(deltaTime: Float) {
print(camera.rotation, camera.distance)
}
}
Collisions
공이 Scene 주변에 부딪히고(충돌이 일어나고) Bouncing되어야 한다.
공이 테두리나 패들과 충돌하면 튕겨 나가야 하고, 벽돌과 충돌하면 벽돌 Instance를 제거해야 한다.
var ballVelocity = SIMD3<Float>(Constants.ballSpeed, 0, Constants.ballSpeed)
func bounceBall() {
if abs(ball.position.x) > gameArea.width / 2 {
ballVelocity.x = -ballVelocity.x
}
if abs(ball.position.z) > gameArea.height / 2 {
ballVelocity.z = -ballVelocity.z
}
if ball.worldBoundingBox().intersects(paddle.worldBoundingBox()) {
ballVelocity.z = -ballVelocity.z
}
}
func checkBricks() {
var brickToDestroyIndex: Int?
for (i, transform) in bricks.transforms.enumerated(){
let modelMatrix = bricks.matrix * transform.matrix
let brickRect = bricks.worldBoundingBox(matrix: modelMatrix)
if ball.worldBoundingBox().intersects(brickRect) {
brickToDestroyIndex = i
break
}
}
if let index = brickToDestroyIndex {
bricks.transforms.remove(at: index)
bricks.instanceCount -= 1
ballVelocity.z = -ballVelocity.z
}
if bricks.instanceCount <= 0 {
remove(node: bricks)
print("GAME OVER - YOU WON!!!!")
}
}
override func updateScene(deltaTime: Float) {
ball.position += ballVelocity * deltaTime
bounceBall()
}
'IT > Graphics' 카테고리의 다른 글
Metal 스터디 8주차 (23.02.02) (0) | 2023.04.16 |
---|---|
Metal 스터디 6주차 (23.01.19) (0) | 2023.02.06 |
Metal 스터디 5주차 (23.01.12) (0) | 2023.01.15 |
Metal 스터디 4주차 (23.01.06) (0) | 2023.01.15 |
Metal 스터디 3주차 (22.12.22) (0) | 2022.12.28 |
댓글