You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Web-Dev-For-Beginners/translations/ko/6-space-game/3-moving-elements-around/README.md

402 lines
15 KiB

<!--
CO_OP_TRANSLATOR_METADATA:
{
"original_hash": "a9a161871de7706cb0e23b1bd0c74559",
"translation_date": "2025-08-29T15:37:37+00:00",
"source_file": "6-space-game/3-moving-elements-around/README.md",
"language_code": "ko"
}
-->
# 우주 게임 만들기 Part 3: 움직임 추가하기
## 강의 전 퀴즈
[강의 전 퀴즈](https://ff-quizzes.netlify.app/web/quiz/33)
게임은 화면에 외계인이 돌아다니기 시작해야 재미있어집니다! 이번 게임에서는 두 가지 움직임을 활용할 것입니다:
- **키보드/마우스 움직임**: 사용자가 키보드나 마우스를 사용해 화면의 객체를 움직이는 경우.
- **게임 유발 움직임**: 게임이 일정 시간 간격으로 객체를 움직이는 경우.
그렇다면 화면에서 객체를 어떻게 움직일까요? 이는 데카르트 좌표계에 관한 것입니다. 객체의 위치 (x, y)를 변경한 후 화면을 다시 그리는 것입니다.
화면에서 *움직임*을 구현하려면 일반적으로 다음 단계를 따라야 합니다:
1. **새로운 위치 설정**: 객체가 움직였다고 인식되려면 새로운 위치를 설정해야 합니다.
2. **화면 지우기**: 화면은 그리기 사이에 지워져야 합니다. 배경색으로 채운 직사각형을 그려 화면을 지울 수 있습니다.
3. **새로운 위치에서 객체 다시 그리기**: 이를 통해 객체를 한 위치에서 다른 위치로 이동시키는 작업이 완료됩니다.
다음은 코드로 표현된 예시입니다:
```javascript
//set the hero's location
hero.x += 5;
// clear the rectangle that hosts the hero
ctx.clearRect(0, 0, canvas.width, canvas.height);
// redraw the game background and hero
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.fillStyle = "black";
ctx.drawImage(heroImg, hero.x, hero.y);
```
✅ 초당 여러 프레임으로 영웅을 다시 그리면 성능 비용이 발생할 수 있는 이유를 생각해볼 수 있나요? [이 패턴의 대안](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Optimizing_canvas)에 대해 읽어보세요.
## 키보드 이벤트 처리하기
이벤트는 특정 이벤트를 코드에 연결하여 처리합니다. 키보드 이벤트는 전체 창에서 트리거되며, 마우스 이벤트(예: `click`)는 특정 요소를 클릭하는 것과 연결될 수 있습니다. 이번 프로젝트에서는 키보드 이벤트를 사용할 것입니다.
이벤트를 처리하려면 창의 `addEventListener()` 메서드를 사용하고 두 개의 입력 매개변수를 제공해야 합니다. 첫 번째 매개변수는 이벤트 이름(예: `keyup`)이고, 두 번째 매개변수는 이벤트가 발생했을 때 호출될 함수입니다.
다음은 예시입니다:
```javascript
window.addEventListener('keyup', (evt) => {
// `evt.key` = string representation of the key
if (evt.key === 'ArrowUp') {
// do something
}
})
```
키 이벤트의 경우, 눌린 키를 확인하기 위해 이벤트에서 사용할 수 있는 두 가지 속성이 있습니다:
- `key`: 눌린 키의 문자열 표현, 예를 들어 `ArrowUp`.
- `keyCode`: 숫자 표현, 예를 들어 `37`, 이는 `ArrowLeft`에 해당합니다.
✅ 키 이벤트 조작은 게임 개발 외에도 유용합니다. 이 기술을 사용할 수 있는 다른 용도를 생각해볼 수 있나요?
### 특수 키: 주의사항
일부 *특수* 키는 창에 영향을 미칩니다. 즉, `keyup` 이벤트를 듣고 이러한 특수 키를 사용해 영웅을 움직이면 수평 스크롤도 수행됩니다. 따라서 게임을 개발하면서 이러한 기본 브라우저 동작을 *비활성화*하고 싶을 수 있습니다. 이를 위해 다음과 같은 코드가 필요합니다:
```javascript
let onKeyDown = function (e) {
console.log(e.keyCode);
switch (e.keyCode) {
case 37:
case 39:
case 38:
case 40: // Arrow keys
case 32:
e.preventDefault();
break; // Space
default:
break; // do not block other keys
}
};
window.addEventListener('keydown', onKeyDown);
```
위 코드는 화살표 키와 스페이스 키의 *기본* 동작을 비활성화합니다. *비활성화* 메커니즘은 `e.preventDefault()`를 호출할 때 발생합니다.
## 게임 유발 움직임
`setTimeout()` 또는 `setInterval()` 함수와 같은 타이머를 사용하여 객체의 위치를 각 틱(시간 간격)마다 업데이트함으로써 객체를 스스로 움직이게 할 수 있습니다. 다음은 그 예시입니다:
```javascript
let id = setInterval(() => {
//move the enemy on the y axis
enemy.y += 10;
})
```
## 게임 루프
게임 루프는 기본적으로 정기적으로 호출되는 함수입니다. 게임 루프라고 불리는 이유는 사용자에게 보여야 할 모든 것이 루프 안에서 그려지기 때문입니다. 게임 루프는 게임의 모든 객체를 사용하여 그리며, 어떤 이유로든 더 이상 게임의 일부가 아닌 객체는 제외합니다. 예를 들어, 레이저에 맞아 폭발한 적 객체는 더 이상 현재 게임 루프의 일부가 아닙니다(이후 강의에서 더 배울 것입니다).
다음은 코드로 표현된 일반적인 게임 루프입니다:
```javascript
let gameLoopId = setInterval(() =>
function gameLoop() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawHero();
drawEnemies();
drawStaticObjects();
}, 200);
```
위 루프는 캔버스를 다시 그리기 위해 매 `200` 밀리초마다 호출됩니다. 게임에 적합한 간격을 선택할 수 있습니다.
## 우주 게임 계속하기
기존 코드를 확장할 것입니다. Part I에서 완료한 코드를 사용하거나 [Part II- starter](../../../../6-space-game/3-moving-elements-around/your-work)의 코드를 사용할 수 있습니다.
- **영웅 움직이기**: 화살표 키를 사용해 영웅을 움직일 수 있도록 코드를 추가합니다.
- **적 움직이기**: 적들이 일정한 속도로 위에서 아래로 움직이도록 코드를 추가합니다.
## 추천 단계
`your-work` 하위 폴더에 생성된 파일을 찾으세요. 다음을 포함해야 합니다:
```bash
-| assets
-| enemyShip.png
-| player.png
-| index.html
-| app.js
-| package.json
```
`your_work` 폴더에서 프로젝트를 시작하려면 다음을 입력하세요:
```bash
cd your-work
npm start
```
위 명령은 `http://localhost:5000` 주소에서 HTTP 서버를 시작합니다. 브라우저를 열고 해당 주소를 입력하세요. 현재는 영웅과 모든 적들이 렌더링되지만 아직 아무것도 움직이지 않습니다!
### 코드 추가하기
1. **`hero`, `enemy`, `game object`에 대한 전용 객체 추가**: 이 객체들은 `x``y` 속성을 가져야 합니다. ([상속 또는 구성](../README.md) 부분을 기억하세요).
*힌트* `game object``x``y` 속성을 가지고 있으며 캔버스에 자신을 그릴 수 있는 능력을 가져야 합니다.
>팁: 아래와 같이 생성자를 가진 새로운 GameObject 클래스를 추가하고 캔버스에 그리기 시작하세요:
```javascript
class GameObject {
constructor(x, y) {
this.x = x;
this.y = y;
this.dead = false;
this.type = "";
this.width = 0;
this.height = 0;
this.img = undefined;
}
draw(ctx) {
ctx.drawImage(this.img, this.x, this.y, this.width, this.height);
}
}
```
이제 이 GameObject를 확장하여 Hero와 Enemy를 만드세요.
```javascript
class Hero extends GameObject {
constructor(x, y) {
...it needs an x, y, type, and speed
}
}
```
```javascript
class Enemy extends GameObject {
constructor(x, y) {
super(x, y);
(this.width = 98), (this.height = 50);
this.type = "Enemy";
let id = setInterval(() => {
if (this.y < canvas.height - this.height) {
this.y += 5;
} else {
console.log('Stopped at', this.y)
clearInterval(id);
}
}, 300)
}
}
```
2. ** 이벤트 핸들러 추가**: 화살표 키를 사용해 영웅을 위/아래, 왼쪽/오른쪽으로 움직이도록 처리합니다.
*기억하세요* 데카르트 시스템에서 좌상단은 `0,0`입니다. 또한 *기본 동작* 중지하는 코드를 추가하는 것을 잊지 마세요.
>팁: onKeyDown 함수를 생성하고 창에 연결하세요:
```javascript
let onKeyDown = function (e) {
console.log(e.keyCode);
...add the code from the lesson above to stop default behavior
}
};
window.addEventListener("keydown", onKeyDown);
```
이 시점에서 브라우저 콘솔을 확인하고 키 입력이 기록되는 것을 확인하세요.
3. **[Pub sub 패턴](../README.md) 구현**: 남은 부분을 따라가면서 코드를 깔끔하게 유지합니다.
이를 위해 다음을 수행할 수 있습니다:
1. **창에 이벤트 리스너 추가**:
```javascript
window.addEventListener("keyup", (evt) => {
if (evt.key === "ArrowUp") {
eventEmitter.emit(Messages.KEY_EVENT_UP);
} else if (evt.key === "ArrowDown") {
eventEmitter.emit(Messages.KEY_EVENT_DOWN);
} else if (evt.key === "ArrowLeft") {
eventEmitter.emit(Messages.KEY_EVENT_LEFT);
} else if (evt.key === "ArrowRight") {
eventEmitter.emit(Messages.KEY_EVENT_RIGHT);
}
});
```
1. **EventEmitter 클래스 생성**: 메시지를 발행하고 구독할 수 있도록 설정합니다:
```javascript
class EventEmitter {
constructor() {
this.listeners = {};
}
on(message, listener) {
if (!this.listeners[message]) {
this.listeners[message] = [];
}
this.listeners[message].push(listener);
}
emit(message, payload = null) {
if (this.listeners[message]) {
this.listeners[message].forEach((l) => l(message, payload));
}
}
}
```
1. **상수 추가** 및 EventEmitter 설정:
```javascript
const Messages = {
KEY_EVENT_UP: "KEY_EVENT_UP",
KEY_EVENT_DOWN: "KEY_EVENT_DOWN",
KEY_EVENT_LEFT: "KEY_EVENT_LEFT",
KEY_EVENT_RIGHT: "KEY_EVENT_RIGHT",
};
let heroImg,
enemyImg,
laserImg,
canvas, ctx,
gameObjects = [],
hero,
eventEmitter = new EventEmitter();
```
1. **게임 초기화**
```javascript
function initGame() {
gameObjects = [];
createEnemies();
createHero();
eventEmitter.on(Messages.KEY_EVENT_UP, () => {
hero.y -=5 ;
})
eventEmitter.on(Messages.KEY_EVENT_DOWN, () => {
hero.y += 5;
});
eventEmitter.on(Messages.KEY_EVENT_LEFT, () => {
hero.x -= 5;
});
eventEmitter.on(Messages.KEY_EVENT_RIGHT, () => {
hero.x += 5;
});
}
```
1. **게임 루프 설정**
window.onload 함수를 리팩터링하여 게임을 초기화하고 적절한 간격으로 게임 루프를 설정합니다. 또한 레이저 빔을 추가합니다:
```javascript
window.onload = async () => {
canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
heroImg = await loadTexture("assets/player.png");
enemyImg = await loadTexture("assets/enemyShip.png");
laserImg = await loadTexture("assets/laserRed.png");
initGame();
let gameLoopId = setInterval(() => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawGameObjects(ctx);
}, 100)
};
```
5. **적들이 일정한 간격으로 움직이도록 코드 추가**
`createEnemies()` 함수를 리팩터링하여 적들을 생성하고 새로운 gameObjects 클래스에 추가합니다:
```javascript
function createEnemies() {
const MONSTER_TOTAL = 5;
const MONSTER_WIDTH = MONSTER_TOTAL * 98;
const START_X = (canvas.width - MONSTER_WIDTH) / 2;
const STOP_X = START_X + MONSTER_WIDTH;
for (let x = START_X; x < STOP_X; x += 98) {
for (let y = 0; y < 50 * 5; y += 50) {
const enemy = new Enemy(x, y);
enemy.img = enemyImg;
gameObjects.push(enemy);
}
}
}
```
그리고 영웅을 위한 `createHero()` 함수를 생성하여 유사한 프로세스를 수행합니다.
```javascript
function createHero() {
hero = new Hero(
canvas.width / 2 - 45,
canvas.height - canvas.height / 4
);
hero.img = heroImg;
gameObjects.push(hero);
}
```
마지막으로 `drawGameObjects()` 함수를 추가하여 그리기를 시작합니다:
```javascript
function drawGameObjects(ctx) {
gameObjects.forEach(go => go.draw(ctx));
}
```
이제 적들이 영웅 우주선으로 다가오기 시작할 것입니다!
---
## 🚀 도전 과제
보시다시피, 함수와 변수, 클래스를 추가하기 시작하면 코드가 '스파게티 코드'로 변할 있습니다. 코드를 읽기 쉽게 만들기 위해 어떻게 조직할 있을까요? 코드가 여전히 하나의 파일에 있어도 시스템을 스케치해보세요.
## 강의 후 퀴즈
[강의 후 퀴즈](https://ff-quizzes.netlify.app/web/quiz/34)
## 복습 및 자기 학습
프레임워크 없이 게임을 작성하고 있지만, 게임 개발을 위한 많은 JavaScript 기반 캔버스 프레임워크가 있습니다. 이에 대해 [읽어보는 시간](https://github.com/collections/javascript-game-engines) 가져보세요.
## 과제
[코드에 주석 달기](assignment.md)
---
**면책 조항**:
문서는 AI 번역 서비스 [Co-op Translator](https://github.com/Azure/co-op-translator) 사용하여 번역되었습니다. 정확성을 위해 최선을 다하고 있으나, 자동 번역에는 오류나 부정확성이 포함될 있습니다. 원본 문서를 해당 언어로 작성된 상태에서 권위 있는 자료로 간주해야 합니다. 중요한 정보의 경우, 전문적인 인간 번역을 권장합니다. 번역 사용으로 인해 발생하는 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다.