
HTML Canvas 게임 중력
HTML Canvas 게임에서 중력 시스템을 구현하는 방법을 알아본다. 현실적인 물리 법칙을 적용하여 게임 오브젝트가 자연스럽게 떨어지고 점프하는 효과를 만드는 기법을 설명한다.
기본 중력 구현
게임 오브젝트에 기본적인 중력 효과를 추가해보자.
<canvas id="myCanvas1" width="480" height="270" style="border:1px solid #d3d3d3;">
브라우저가 HTML5 Canvas를 지원하지 않습니다.
</canvas>
<script>
var myGamePiece;
var myGameArea = {
canvas: document.getElementById("myCanvas1"),
start: function() {
this.canvas.width = 480;
this.canvas.height = 270;
this.context = this.canvas.getContext("2d");
this.interval = setInterval(updateGameArea, 20);
},
clear: function() {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
}
function component(width, height, color, x, y) {
this.width = width;
this.height = height;
this.speedX = 0;
this.speedY = 0;
this.x = x;
this.y = y;
this.gravity = 0.05;
this.gravitySpeed = 0;
this.update = function() {
ctx = myGameArea.context;
ctx.fillStyle = color;
ctx.fillRect(this.x, this.y, this.width, this.height);
}
this.newPos = function() {
this.gravitySpeed += this.gravity;
this.x += this.speedX;
this.y += this.speedY + this.gravitySpeed;
this.hitBottom();
}
this.hitBottom = function() {
var rockbottom = myGameArea.canvas.height - this.height;
if (this.y > rockbottom) {
this.y = rockbottom;
this.gravitySpeed = 0;
}
}
}
function startGame() {
myGamePiece = new component(30, 30, "red", 10, 50);
myGameArea.start();
}
function updateGameArea() {
myGameArea.clear();
myGamePiece.newPos();
myGamePiece.update();
}
startGame();
</script>
gravity 속성은 중력의 강도를, gravitySpeed는 중력에 의한 현재 속도를 나타낸다. 매 프레임마다 중력 속도가 누적되어 자연스러운 낙하 효과를 만든다.
점프 기능 추가
키보드 입력으로 점프할 수 있는 기능을 추가해보자.
<canvas id="myCanvas2" width="480" height="270" style="border:1px solid #d3d3d3;">
브라우저가 HTML5 Canvas를 지원하지 않습니다.
</canvas>
<script>
var myGamePiece;
var myGameArea = {
canvas: document.getElementById("myCanvas2"),
start: function() {
this.canvas.width = 480;
this.canvas.height = 270;
this.context = this.canvas.getContext("2d");
this.interval = setInterval(updateGameArea, 20);
window.addEventListener('keydown', function (e) {
myGameArea.key = e.keyCode;
})
window.addEventListener('keyup', function (e) {
myGameArea.key = false;
})
},
clear: function() {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
}
function component(width, height, color, x, y) {
this.width = width;
this.height = height;
this.speedX = 0;
this.speedY = 0;
this.x = x;
this.y = y;
this.gravity = 0.1;
this.gravitySpeed = 0;
this.onGround = false;
this.update = function() {
ctx = myGameArea.context;
ctx.fillStyle = color;
ctx.fillRect(this.x, this.y, this.width, this.height);
}
this.newPos = function() {
this.gravitySpeed += this.gravity;
this.x += this.speedX;
this.y += this.speedY + this.gravitySpeed;
this.hitBottom();
}
this.hitBottom = function() {
var rockbottom = myGameArea.canvas.height - this.height;
if (this.y > rockbottom) {
this.y = rockbottom;
this.gravitySpeed = 0;
this.onGround = true;
} else {
this.onGround = false;
}
}
this.jump = function() {
if (this.onGround) {
this.gravitySpeed = -3;
this.onGround = false;
}
}
}
function startGame() {
myGamePiece = new component(30, 30, "red", 10, 50);
myGameArea.start();
}
function updateGameArea() {
myGameArea.clear();
myGamePiece.speedX = 0;
if (myGameArea.key && myGameArea.key == 37) {myGamePiece.speedX = -2; }
if (myGameArea.key && myGameArea.key == 39) {myGamePiece.speedX = 2; }
if (myGameArea.key && myGameArea.key == 32) {myGamePiece.jump(); }
myGamePiece.newPos();
myGamePiece.update();
}
startGame();
</script>
onGround 속성으로 바닥에 있는지 확인하고, jump() 메서드로 점프를 구현한다. 스페이스바를 누르면 중력 속도를 음수로 설정하여 위로 튀어오르는 효과를 만든다.
플랫폼과 충돌 감지
플랫폼을 추가하고 중력이 적용된 충돌 감지를 구현해보자.
<canvas id="myCanvas3" width="480" height="270" style="border:1px solid #d3d3d3;">
브라우저가 HTML5 Canvas를 지원하지 않습니다.
</canvas>
<script>
var myGamePiece;
var myPlatforms = [];
var myGameArea = {
canvas: document.getElementById("myCanvas3"),
start: function() {
this.canvas.width = 480;
this.canvas.height = 270;
this.context = this.canvas.getContext("2d");
this.interval = setInterval(updateGameArea, 20);
window.addEventListener('keydown', function (e) {
myGameArea.key = e.keyCode;
})
window.addEventListener('keyup', function (e) {
myGameArea.key = false;
})
},
clear: function() {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
}
function component(width, height, color, x, y, type) {
this.type = type;
this.width = width;
this.height = height;
this.speedX = 0;
this.speedY = 0;
this.x = x;
this.y = y;
this.gravity = 0.1;
this.gravitySpeed = 0;
this.onGround = false;
this.update = function() {
ctx = myGameArea.context;
ctx.fillStyle = color;
ctx.fillRect(this.x, this.y, this.width, this.height);
}
this.newPos = function() {
this.gravitySpeed += this.gravity;
this.x += this.speedX;
this.y += this.speedY + this.gravitySpeed;
this.checkCollisions();
}
this.checkCollisions = function() {
var rockbottom = myGameArea.canvas.height - this.height;
this.onGround = false;
if (this.y > rockbottom) {
this.y = rockbottom;
this.gravitySpeed = 0;
this.onGround = true;
}
for (var i = 0; i < myPlatforms.length; i++) {
var platform = myPlatforms[i];
if (this.crashWith(platform) && this.gravitySpeed > 0) {
if (this.y < platform.y) {
this.y = platform.y - this.height;
this.gravitySpeed = 0;
this.onGround = true;
}
}
}
}
this.crashWith = function(otherobj) {
var myleft = this.x;
var myright = this.x + (this.width);
var mytop = this.y;
var mybottom = this.y + (this.height);
var otherleft = otherobj.x;
var otherright = otherobj.x + (otherobj.width);
var othertop = otherobj.y;
var otherbottom = otherobj.y + (otherobj.height);
var crash = true;
if ((mybottom < othertop) || (mytop > otherbottom) || (myright < otherleft) || (myleft > otherright)) {
crash = false;
}
return crash;
}
this.jump = function() {
if (this.onGround) {
this.gravitySpeed = -4;
this.onGround = false;
}
}
}
function startGame() {
myGamePiece = new component(30, 30, "red", 10, 50);
myPlatforms.push(new component(100, 20, "green", 150, 200, "platform"));
myPlatforms.push(new component(80, 20, "green", 300, 150, "platform"));
myPlatforms.push(new component(60, 20, "green", 400, 100, "platform"));
myGameArea.start();
}
function updateGameArea() {
myGameArea.clear();
myGamePiece.speedX = 0;
if (myGameArea.key && myGameArea.key == 37) {myGamePiece.speedX = -2; }
if (myGameArea.key && myGameArea.key == 39) {myGamePiece.speedX = 2; }
if (myGameArea.key && myGameArea.key == 32) {myGamePiece.jump(); }
myGamePiece.newPos();
myGamePiece.update();
for (var i = 0; i < myPlatforms.length; i++) {
myPlatforms[i].update();
}
}
startGame();
</script>[/code]
플랫폼과의 충돌을 감지하여 플레이어가 플랫폼 위에 착지할 수 있게 한다. 중력 속도가 양수일 때(하강 중)만 플랫폼에 착지하도록 조건을 설정했다.
반발력과 탄성
오브젝트가 바닥이나 플랫폼에 부딪혔을 때 반발하는 효과를 추가해보자.
[code lang="markup"]<canvas id="myCanvas4" width="480" height="270" style="border:1px solid #d3d3d3;">
브라우저가 HTML5 Canvas를 지원하지 않습니다.
</canvas>
<script>
var myGamePiece;
var myGameArea = {
canvas: document.getElementById("myCanvas4"),
start: function() {
this.canvas.width = 480;
this.canvas.height = 270;
this.context = this.canvas.getContext("2d");
this.interval = setInterval(updateGameArea, 20);
},
clear: function() {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
}
function component(width, height, color, x, y) {
this.width = width;
this.height = height;
this.speedX = 0;
this.speedY = 0;
this.x = x;
this.y = y;
this.gravity = 0.1;
this.gravitySpeed = 0;
this.bounce = 0.7;
this.friction = 0.98;
this.update = function() {
ctx = myGameArea.context;
ctx.fillStyle = color;
ctx.fillRect(this.x, this.y, this.width, this.height);
}
this.newPos = function() {
this.gravitySpeed += this.gravity;
this.gravitySpeed *= this.friction;
this.speedX *= this.friction;
this.x += this.speedX;
this.y += this.speedY + this.gravitySpeed;
this.hitBottom();
this.hitSides();
}
this.hitBottom = function() {
var rockbottom = myGameArea.canvas.height - this.height;
if (this.y > rockbottom) {
this.y = rockbottom;
if (Math.abs(this.gravitySpeed) > 0.1) {
this.gravitySpeed = -this.gravitySpeed * this.bounce;
} else {
this.gravitySpeed = 0;
}
}
}
this.hitSides = function() {
if (this.x < 0) {
this.x = 0;
this.speedX = -this.speedX * this.bounce;
}
if (this.x > myGameArea.canvas.width - this.width) {
this.x = myGameArea.canvas.width - this.width;
this.speedX = -this.speedX * this.bounce;
}
}
}
function startGame() {
myGamePiece = new component(30, 30, "red", 100, 50);
myGamePiece.speedX = 3;
myGameArea.start();
}
function updateGameArea() {
myGameArea.clear();
myGamePiece.newPos();
myGamePiece.update();
}
startGame();
</script>
bounce 속성은 반발 계수를, friction 속성은 마찰력을 나타낸다. 바닥이나 벽에 부딪히면 속도가 반대 방향으로 바뀌고 반발 계수만큼 감소한다.
가변 중력
중력의 강도를 조절할 수 있는 시스템을 구현해보자.
<canvas id="myCanvas5" width="480" height="270" style="border:1px solid #d3d3d3;">
브라우저가 HTML5 Canvas를 지원하지 않습니다.
</canvas>
<button onclick="changeGravity(0.05)">약한 중력</button>
<button onclick="changeGravity(0.1)">보통 중력</button>
<button onclick="changeGravity(0.2)">강한 중력</button>
<button onclick="changeGravity(-0.05)">역중력</button>
<script>
var myGamePiece;
var currentGravity = 0.1;
var myGameArea = {
canvas: document.getElementById("myCanvas5"),
start: function() {
this.canvas.width = 480;
this.canvas.height = 270;
this.context = this.canvas.getContext("2d");
this.interval = setInterval(updateGameArea, 20);
window.addEventListener('keydown', function (e) {
myGameArea.key = e.keyCode;
})
window.addEventListener('keyup', function (e) {
myGameArea.key = false;
})
},
clear: function() {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
}
function component(width, height, color, x, y) {
this.width = width;
this.height = height;
this.speedX = 0;
this.speedY = 0;
this.x = x;
this.y = y;
this.gravity = currentGravity;
this.gravitySpeed = 0;
this.onGround = false;
this.update = function() {
ctx = myGameArea.context;
ctx.fillStyle = color;
ctx.fillRect(this.x, this.y, this.width, this.height);
}
this.newPos = function() {
this.gravity = currentGravity;
this.gravitySpeed += this.gravity;
this.x += this.speedX;
this.y += this.speedY + this.gravitySpeed;
this.checkBounds();
}
this.checkBounds = function() {
var rockbottom = myGameArea.canvas.height - this.height;
var rocktop = 0;
this.onGround = false;
if (this.gravity > 0) {
if (this.y > rockbottom) {
this.y = rockbottom;
this.gravitySpeed = 0;
this.onGround = true;
}
} else {
if (this.y < rocktop) {
this.y = rocktop;
this.gravitySpeed = 0;
this.onGround = true;
}
}
}
this.jump = function() {
if (this.onGround) {
if (this.gravity > 0) {
this.gravitySpeed = -4;
} else {
this.gravitySpeed = 4;
}
this.onGround = false;
}
}
}
function startGame() {
myGamePiece = new component(30, 30, "red", 225, 135);
myGameArea.start();
}
function updateGameArea() {
myGameArea.clear();
myGamePiece.speedX = 0;
if (myGameArea.key && myGameArea.key == 37) {myGamePiece.speedX = -2; }
if (myGameArea.key && myGameArea.key == 39) {myGamePiece.speedX = 2; }
if (myGameArea.key && myGameArea.key == 32) {myGamePiece.jump(); }
myGamePiece.newPos();
myGamePiece.update();
}
function changeGravity(newGravity) {
currentGravity = newGravity;
}
startGame();
</script>
중력의 방향과 강도를 동적으로 변경할 수 있다. 역중력을 설정하면 오브젝트가 위쪽으로 떨어지고 천장에 착지한다.
다중 오브젝트 중력
여러 오브젝트에 서로 다른 중력 설정을 적용해보자.
<canvas id="myCanvas6" width="480" height="270" style="border:1px solid #d3d3d3;">브라우저가 HTML5 Canvas를 지원하지 않습니다.
</canvas>
<script>
var myGamePieces = [];
var myGameArea = {
canvas: document.getElementById("myCanvas6"),
start: function() {
this.canvas.width = 480;
this.canvas.height = 270;
this.context = this.canvas.getContext("2d");
this.interval = setInterval(updateGameArea, 20);
},
clear: function() {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
}
function component(width, height, color, x, y, gravity, bounce) {
this.width = width;
this.height = height;
this.speedX = 0;
this.speedY = 0;
this.x = x;
this.y = y;
this.gravity = gravity || 0.1;
this.gravitySpeed = 0;
this.bounce = bounce || 0.6;
this.friction = 0.99;
this.update = function() {
ctx = myGameArea.context;
ctx.fillStyle = color;
ctx.fillRect(this.x, this.y, this.width, this.height);
}
this.newPos = function() {
this.gravitySpeed += this.gravity;
this.gravitySpeed *= this.friction;
this.speedX *= this.friction;
this.x += this.speedX;
this.y += this.speedY + this.gravitySpeed;
this.hitBounds();
}
this.hitBounds = function() {
var rockbottom = myGameArea.canvas.height - this.height;
var rocktop = 0;
var leftwall = 0;
var rightwall = myGameArea.canvas.width - this.width;
if (this.y > rockbottom) {
this.y = rockbottom;
if (Math.abs(this.gravitySpeed) > 0.1) {
this.gravitySpeed = -this.gravitySpeed * this.bounce;
} else {
this.gravitySpeed = 0;
}
}
if (this.y < rocktop) { this.y = rocktop; if (Math.abs(this.gravitySpeed) > 0.1) {
this.gravitySpeed = -this.gravitySpeed * this.bounce;
} else {
this.gravitySpeed = 0;
}
}
if (this.x < leftwall) { this.x = leftwall; this.speedX = -this.speedX * this.bounce; } if (this.x > rightwall) {
this.x = rightwall;
this.speedX = -this.speedX * this.bounce;
}
}
}
function startGame() {
myGamePieces.push(new component(20, 20, "red", 50, 50, 0.05, 0.9));
myGamePieces.push(new component(25, 25, "blue", 150, 50, 0.1, 0.7));
myGamePieces.push(new component(30, 30, "green", 250, 50, 0.15, 0.5));
myGamePieces.push(new component(35, 35, "purple", 350, 50, 0.2, 0.3));
for (var i = 0; i < myGamePieces.length; i++) { myGamePieces[i].speedX = (Math.random() - 0.5) * 4; myGamePieces[i].speedY = (Math.random() - 0.5) * 2; } myGameArea.start(); } function updateGameArea() { myGameArea.clear(); for (var i = 0; i < myGamePieces.length; i++) { myGamePieces[i].newPos(); myGamePieces[i].update(); } } startGame(); </script>[/code]
각 오브젝트마다 다른 중력과 반발 계수를 설정하여 다양한 물리적 특성을 보여준다. 무거운 오브젝트는 빠르게 떨어지고 적게 튀어오른다.
중력 영역
특정 영역에서만 중력이 작용하는 시스템을 구현해보자.
<canvas id="myCanvas7" width="480" height="270" style="border:1px solid #d3d3d3;">브라우저가 HTML5 Canvas를 지원하지 않습니다.
</canvas>
<script>
var myGamePiece;
var gravityZones = [];
var myGameArea = {
canvas: document.getElementById("myCanvas7"),
start: function() {
this.canvas.width = 480;
this.canvas.height = 270;
this.context = this.canvas.getContext("2d");
this.interval = setInterval(updateGameArea, 20);
window.addEventListener('keydown', function (e) {
myGameArea.key = e.keyCode;
})
window.addEventListener('keyup', function (e) {
myGameArea.key = false;
})
},
clear: function() {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
}
function component(width, height, color, x, y, type) {
this.type = type;
this.width = width;
this.height = height;
this.speedX = 0;
this.speedY = 0;
this.x = x;
this.y = y;
this.gravity = 0;
this.gravitySpeed = 0;
this.defaultGravity = 0.1;
this.update = function() {
ctx = myGameArea.context;
if (this.type == "gravityzone") {
ctx.fillStyle = "rgba(255, 255, 0, 0.3)";
ctx.fillRect(this.x, this.y, this.width, this.height);
ctx.strokeStyle = "orange";
ctx.strokeRect(this.x, this.y, this.width, this.height);
} else {
ctx.fillStyle = color;
ctx.fillRect(this.x, this.y, this.width, this.height);
}
}
this.newPos = function() {
this.checkGravityZones();
this.gravitySpeed += this.gravity;
this.x += this.speedX;
this.y += this.speedY + this.gravitySpeed;
this.hitBottom();
}
this.checkGravityZones = function() {
this.gravity = 0;
for (var i = 0; i < gravityZones.length; i++) { if (this.inZone(gravityZones[i])) { this.gravity = gravityZones[i].gravityStrength; break; } } } this.inZone = function(zone) { return (this.x < zone.x + zone.width && this.x + this.width > zone.x &&
this.y < zone.y + zone.height && this.y + this.height > zone.y);
}
this.hitBottom = function() {
var rockbottom = myGameArea.canvas.height - this.height;
if (this.y > rockbottom) {
this.y = rockbottom;
this.gravitySpeed = 0;
}
}
}
function gravityZone(x, y, width, height, strength) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.gravityStrength = strength;
this.component = new component(width, height, "yellow", x, y, "gravityzone");
this.update = function() {
this.component.update();
}
}
function startGame() {
myGamePiece = new component(30, 30, "red", 10, 50);
gravityZones.push(new gravityZone(100, 100, 150, 100, 0.2));
gravityZones.push(new gravityZone(300, 50, 100, 150, -0.1));
myGameArea.start();
}
function updateGameArea() {
myGameArea.clear();
for (var i = 0; i < gravityZones.length; i++) { gravityZones[i].update(); } myGamePiece.speedX = 0; if (myGameArea.key && myGameArea.key == 37) {myGamePiece.speedX = -2; } if (myGameArea.key && myGameArea.key == 39) {myGamePiece.speedX = 2; } myGamePiece.newPos(); myGamePiece.update(); } startGame(); </script>[/code]
중력 영역을 시각적으로 표시하고, 플레이어가 해당 영역에 들어가면 다른 중력이 적용된다. 노란색 영역은 강한 중력, 주황색 테두리 영역은 역중력이 작용한다.
궤도 중력
중심점을 기준으로 하는 궤도 중력 시스템을 구현해보자.
<canvas id="myCanvas8" width="480" height="270" style="border:1px solid #d3d3d3;">브라우저가 HTML5 Canvas를 지원하지 않습니다.
</canvas>
<script>
var myGamePieces = [];
var gravityCenters = [];
var myGameArea = {
canvas: document.getElementById("myCanvas8"),
start: function() {
this.canvas.width = 480;
this.canvas.height = 270;
this.context = this.canvas.getContext("2d");
this.interval = setInterval(updateGameArea, 20);
},
clear: function() {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
}
function component(width, height, color, x, y, type) {
this.type = type;
this.width = width;
this.height = height;
this.speedX = 0;
this.speedY = 0;
this.x = x;
this.y = y;
this.mass = width * height / 100;
this.update = function() {
ctx = myGameArea.context;
if (this.type == "gravitycenter") {
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(this.x + this.width/2, this.y + this.height/2, this.width/2, 0, 2 * Math.PI);
ctx.fill();
} else {
ctx.fillStyle = color;
ctx.fillRect(this.x, this.y, this.width, this.height);
}
}
this.newPos = function() {
if (this.type != "gravitycenter") {
this.applyGravity();
}
this.x += this.speedX;
this.y += this.speedY;
this.checkBounds();
}
this.applyGravity = function() {
for (var i = 0; i < gravityCenters.length; i++) { var center = gravityCenters[i]; var dx = (center.x + center.width/2) - (this.x + this.width/2); var dy = (center.y + center.height/2) - (this.y + this.height/2); var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 0) {
var force = (center.mass * this.mass) / (distance * distance) * 0.1;
var forceX = (dx / distance) * force;
var forceY = (dy / distance) * force;
this.speedX += forceX / this.mass;
this.speedY += forceY / this.mass;
}
}
}
this.checkBounds = function() {
if (this.x < 0 || this.x > myGameArea.canvas.width - this.width) {
this.speedX = -this.speedX * 0.8;
this.x = Math.max(0, Math.min(this.x, myGameArea.canvas.width - this.width));
}
if (this.y < 0 || this.y > myGameArea.canvas.height - this.height) {
this.speedY = -this.speedY * 0.8;
this.y = Math.max(0, Math.min(this.y, myGameArea.canvas.height - this.height));
}
}
}
function startGame() {
gravityCenters.push(new component(40, 40, "orange", 200, 100, "gravitycenter"));
gravityCenters.push(new component(30, 30, "yellow", 350, 150, "gravitycenter"));
myGamePieces.push(new component(10, 10, "red", 50, 50));
myGamePieces.push(new component(15, 15, "blue", 400, 50));
myGamePieces.push(new component(12, 12, "green", 100, 200));
for (var i = 0; i < myGamePieces.length; i++) { myGamePieces[i].speedX = (Math.random() - 0.5) * 2; myGamePieces[i].speedY = (Math.random() - 0.5) * 2; } myGameArea.start(); } function updateGameArea() { myGameArea.clear(); for (var i = 0; i < gravityCenters.length; i++) { gravityCenters[i].update(); } for (var i = 0; i < myGamePieces.length; i++) { myGamePieces[i].newPos(); myGamePieces[i].update(); } } startGame(); </script>[/code]
중력 중심점들이 주변 오브젝트들을 끌어당기는 궤도 중력을 구현했다. 거리의 제곱에 반비례하는 만유인력 법칙을 적용하여 현실적인 궤도 운동을 만든다.
💡 게임 중력 시스템 설계 팁:
• 중력 값은 게임의 느낌에 맞게 조절해야 한다. 너무 강하면 조작이 어려워지고, 너무 약하면 둔해 보인다
• 점프 높이와 중력의 균형을 맞춰서 적절한 게임플레이를 만들어야 한다
• 마찰력과 반발력을 함께 사용하면 더 현실적인 물리 효과를 얻을 수 있다
• 성능을 고려하여 복잡한 물리 계산은 필요한 경우에만 사용하자
HTML5 Canvas 게임에서 중력 시스템은 게임에 현실감과 역동성을 더하는 중요한 요소이다. 기본적인 중력부터 복잡한 궤도 시스템까지 다양한 물리 효과를 통해 매력적인 게임 경험을 만들 수 있다.