[JS] ๋ฐ๋๋ผ ์๋ฐ์คํฌ๋ฆฝํธ๋ก ๋ง๋ฆฌ์ค ๋ฌ๋ ๊ฒ์ ๊ตฌํํ๊ธฐ
๋ฌ๋ ๊ฒ์(๋ฌ๋ฆฌ๊ธฐ ๊ฒ์)์ ํ๋ ์ด์ด๊ฐ ์๋์ผ๋ก ์ ์งํ๋ฉด์ ์ฅ์ ๋ฌผ์ ํํผํ๊ณ , ์ ์๋ฅผ ํ๋ํ๋ ๊ฒ์ ์ ํ์ด๋ค. ๊ฐ๋จํ ๋ฌ๋ ๊ฒ์์ ์บ๋ฒ์ค ์์ด JavaScript, HTML, CSS ๋ง์ผ๋ก๋ ๊ตฌํํ ์ ์๋ค. ๋ฌ๋ ๊ฒ์ ๊ตฌํ์ ํฌ๊ฒ โ ์ด๋(์ ์ง), โก์ ํ, โข์ฅ์ ๋ฌผ ๋ฐฐ์น, โฃ์ฅ์ ๋ฌผ ์ถฉ๋ ๊ฐ์ง๋ก ๋๋ ์ ์๋ค. ์ํผ ๋ง๋ฆฌ์ค๋ ํ๋ ์ด์ด๊ฐ ์บ๋ฆญํฐ๋ฅผ ์ง์ ์กฐ์ข ํ๋ ํ๋ซํฌ๋จธ ์ฅ๋ฅด์ ์ํ์ง๋ง ์น์ํ ๋ง๋ฆฌ์ค ์บ๋ฆญํฐ์ ๊ตฌ์กฐ๋ฌผ์ ์ด์ฉํด์ ๋ฌ๋ ๊ฒ์์ผ๋ก ๋ง๋ค์ด๋ณด์.
์ฑ๊ธํค DOM ๊ด๋ฆฌ
// dom-manager.js
class DomManager {
static instance = null;
constructor() {
// ์์ฑ์ ํจ์๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์๋ก ์์ฑ๋ ์ธ์คํด์ค(this)๋ฅผ ๋ฐํํ์ง๋ง ๋ช
์์ ์ผ๋ก ๋ฐํ ๊ฐ์ ์ง์ ํ ์๋ ์๋ค
if (DomManager.instance) return DomManager.instance;
this.gameArea = document.querySelector('.game');
this.dialog = document.querySelector('.dialog-failed');
this.score = document.querySelector('.score');
this.dialogScore = document.querySelector('.score-dialog');
this.stopButton = document.querySelector('.button-stop');
this.startButton = document.querySelector('.button-start');
this.restartButton = document.querySelector('.button-restart');
DomManager.instance = this;
}
static getInstance() {
// getInstance()๋ฅผ ์ฒ์ ํธ์ถํ๋ฉด ์์ฑ์ ํจ์๋ฅผ ์คํํด์ ์๋ก์ด ์ธ์คํด์ค๋ฅผ ๋ง๋ค๊ณ DomManager.instance์ ํ ๋นํ๋ค
// ๊ทธ ํ getInstance()๋ฅผ ๋ค์ ํธ์ถํ๋ฉด DomManager.instance ์ ์ ํ๋กํผํฐ์ ํ ๋นํ๋ ๊ธฐ์กด ์ธ์คํด์ค๋ฅผ ๋ฐํํ๋ค
if (!DomManager.instance) DomManager.instance = new DomManager();
return DomManager.instance;
}
}
// ํ๋ก์ ํธ์์ ๋์ผํ DomManager ์ธ์คํด์ค์ ์ ๊ทผํ ์ ์๋๋ก ์ฑ๊ธํค์ผ๋ก export
export default DomManager.getInstance();
// dom-manager.js
class DomManager {
static instance = null;
constructor() {
if (DomManager.instance) return DomManager.instance;
this.gameArea = document.querySelector('.game');
this.dialog = document.querySelector('.dialog-failed');
// ...
DomManager.instance = this;
}
static getInstance() {
if (!DomManager.instance) DomManager.instance = new DomManager();
return DomManager.instance;
}
}
export default DomManager.getInstance();
๊ฒ์ ์์ญ, ์ค์ฝ์ด, ์์ ๋ฒํผ ๋ฑ ๊ฒ์์์ ์์ฃผ ์ฌ์ฉํ๋ ์์๋ค์ DomManager
๋ผ๋ ํด๋์ค์์ ๊ด๋ฆฌํ๋ค. ์ด๋ ๊ฒ DOM ๊ด๋ จ ์์
์ ํ ๊ณณ์ ์ค์ํํ๋ฉด ๋ฐ๋ณต์ ์ธ ์ฟผ๋ฆฌ์ ์กฐ์์ ํผํ ์ ์๋ค.
ํด๋์ค์ ์ธ์คํด์ค๋ฅผ ์ค์ง ํ๋๋ง ์์ฑํ์ฌ ์ดํ๋ฆฌ์ผ์ด์ ์ ์ฒด์ ๊ณต์ ํ๋ ๋ฐฉ์์ ์ฑ๊ธํค ํจํด์ด๋ผ๊ณ ๋ถ๋ฅธ๋ค. ๋ ๋งค๋์ ๋ฅผ ์ฑ๊ธํค์ผ๋ก ๋ด๋ณด๋ด๋ฉด ํ๋ก์ ํธ ์ด๋์๋ ๋์ผํ ์ธ์คํด์ค์ ์ ๊ทผํ๊ธฐ ๋๋ฌธ์ ์ผ๊ด์ฑ์ ๋์ผ ์ ์๋ค.
getInstance()
์ ์ ๋ฉ์๋๋ฅผ ์ฒ์ ํธ์ถํ๋ฉด ์์ฑํ ์ธ์คํด์ค๊ฐ ์๊ธฐ ๋๋ฌธ์ constructor
๋ฅผ ์คํํด์ ์๋ก์ด ์ธ์คํด์ค๋ฅผ ๋ง๋ค๊ณ , DomManager.instance
์ ์ ํ๋กํผํฐ์ ํ ๋นํ๋ค. ๊ทธ ํ getInstance()
๋ฅผ ๋ค์ ํธ์ถํ๋ฉด DomManager.instance
ํ๋กํผํฐ์ ํ ๋นํ๋ ๊ธฐ์กด ์ธ์คํด์ค๋ฅผ ๋ฐํํ๋ค.
๐ก ์์ฑ์(constructor) ํจ์๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์๋ก ์์ฑ๋ ์ธ์คํด์ค(this)๋ฅผ ๋ฐํํ์ง๋ง ์์ฒ๋ผ ๋ช ์์ ์ผ๋ก ๋ฐํ ๊ฐ์ ์ง์ ํ ์๋ ์๋ค.
๋ง๋ฆฌ์ค ์ ์ง
// background.js
class Background {
speed;
positionX = 0;
frameId = null;
constructor({ speed }) {
this.speed = speed;
this.move = this.move.bind(this);
}
move() {
this.positionX -= this.speed;
DomManager.gameArea.style.backgroundPositionX = this.positionX + 'px';
this.frameId = requestAnimationFrame(this.move);
}
stop() {
cancelAnimationFrame(this.frameId);
this.frameId = null;
}
reset() {
this.positionX = 0;
DomManager.gameArea.style.backgroundPositionX = this.positionX + 'px';
}
}
export default Background;
๊ฒ์๋ด ๊ตฌ์กฐ๋ฌผ๋ค์ด ๊ฒ์ ํ๋ฉด์ ๊ธฐ์ค์ผ๋ก ์์ง์ผ ์ ์๋๋ก, ๊ฒ์ ํ๋ฉด ์์์ position
๊ฐ์ relative
๋ก ์ค์ ํ๊ณ , ๋ง๋ฆฌ์ค๋ ์ฅ์ ๋ฌผ ๋ฑ์ absolute
๋ก ์ค์ ํ๋ค.
/* ๊ฒ์ ํ๋ฉด(gameArea) ์์์ CSS ์คํ์ผ๋ง */
.game {
width: 100%;
min-height: 440px;
overflow: hidden;
position: relative;
background-image: url('./assets/background.png');
background-size: 600px 500px;
background-position: center;
}
background-position-x
๋ ๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง์ ์ํ ์์น๋ฅผ ์กฐ์ ํ ๋ ์ฌ์ฉํ๋ ์คํ์ผ ์์ฑ์ด๋ค. ์ด ์์ฑ์ ์ด์ฉํด ๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง์ ์ํ ์์น๋ฅผ ์ฃผ๊ธฐ์ ์ผ๋ก ์ผ์ชฝ์ผ๋ก ์ด๋ ์ํค๋ฉด ์์ผ๋ก ์ ์งํ๋ ๋ฏํ ํจ๊ณผ๋ฅผ ๊ตฌํํ ์ ์๋ค. ์ด ์์ฑ์ ๊ธฐ๋ณธ๊ฐ์ ์ผ์ชฝ ๊ฐ์ฅ ์๋ฆฌ๋ฅผ ๋ํ๋ด๋ 0%
๋ค.
// background.js
class Background {
speed;
positionX = 0;
frameId = null;
constructor({ speed }) {
this.speed = speed;
this.move = this.move.bind(this);
}
move() {
this.positionX -= this.speed;
DomManager.gameArea.style.backgroundPositionX = this.positionX + 'px';
this.frameId = requestAnimationFrame(this.move);
}
// ...
}
์ ์ง ํจ๊ณผ๋ฅผ ์์ฐ์ค๋ฝ๊ฒ ๊ตฌํํ๊ธฐ ์ํด requestAnimationFrame
(rAF) ๋ฉ์๋๋ฅผ ํ์ฉํ์ฌ ๋งค ํ๋ ์๋ง๋ค 5px์ฉ background-position-x
๊ฐ์ ๊ฐ์์ํจ๋ค. rAF ์ฝ๋ฐฑ ํจ์์ ํธ์ถ ์ฃผ๊ธฐ๋ ๋ชจ๋ํฐ ์ฃผ์ฌ์จ์ ๋ฐ๋ผ ๊ฒฐ์ ๋๋๋ฐ, 60Hz ์ฃผ์ฌ์จ์ ๊ฐ๋(1์ด์ ํ๋ฉด์ 60๋ฒ ์๋ก ๊ณ ์นจ) ์ผ๋ฐ์ ์ธ ๋ชจ๋ํฐ๋ฅผ ๊ธฐ์ค์ผ๋ก 1์ด์ 60ํ ํธ์ถ๋๋ค. ์ฆ, 16.6ms๋ง๋ค ํ ๋ฒ์ฉ ํธ์ถ๋๋ค.
์ค๋ ฅ ์ ํ โญ
// mario.js
class Mario {
static JUMP_HEIGHT = 18; // ์ ํ ๋์ด. ๋์์๋ก ๋ ๋์ด ์ ํ
static GRAVITY = 0.4; // ์ค๋ ฅ ๊ฐ์๋. ๋ฎ์์๋ก ๋ ์ค๋ ์ ํ
static STOP_IMAGE_PATH = './assets/mario-stop.png';
static RUN_IMAGE_PATH = './assets/mario-run.gif';
stopImage = new Image();
runImage = new Image();
element = new Image();
audio;
defaultBottom;
isJumping = false;
constructor({ audio, defaultBottom, className = 'mario' }) {
this.audio = audio;
this.defaultBottom = defaultBottom;
this.preloadImages()
.then(() => this.initializeImage(className))
.catch((error) => console.error('Error initializing Mario:', error));
}
initializeImage(className) {
this.element.src = this.stopImage.src;
this.element.classList.add(className);
this.element.style.bottom = this.defaultBottom + 'px';
DomManager.gameArea.appendChild(this.element);
}
async preloadImages() {
const srcset = [Mario.STOP_IMAGE_PATH, Mario.RUN_IMAGE_PATH];
try {
const [stopImage, runImage] = await loadImages(srcset);
this.stopImage.src = stopImage.src;
this.runImage.src = runImage.src;
} catch (error) {
console.error('Error preloading Mario images:', error);
}
}
run() {
this.element.src = this.runImage.src;
}
stop() {
this.element.src = this.stopImage.src;
}
jump() {
if (this.isJumping) return;
this.audio.playJumpSound();
this.isJumping = true;
let jumpCount = 0;
let velocity = Mario.JUMP_HEIGHT;
/**
* ์์น(Fast) -> ์ ์ (Slow) -> ํ๊ฐ(Fast) ์ค๋ ฅ ์์ฉ์ด ์ ์ฌํ๊ฒ ์ ์ฉ๋ ์ ํ ๋ฉ์๋
* count 1, velocity 17.6, height 17.6 | ์ฐจ์ด 18 -- ์์น
* count 5, velocity 16, height 80 | ์ฐจ์ด 63
* count 10, velocity 14, height 140 | ์ฐจ์ด 60 -- ๋๋ ค์ง๊ธฐ ์์
* count 15, velocity 12, height 180 | ์ฐจ์ด 40
* count 20, velocity 10, height 200 | ์ฐจ์ด 20
* count 22, velocity 9.2, height 202 | ์ฐจ์ด 2 -- ์ ์
* count 25, velocity 8, height 200 | ์ฐจ์ด 2 -- ํ๊ฐ
* count 30, velocity 6, height 180 | ์ฐจ์ด 20 -- ๋นจ๋ผ์ง๊ธฐ ์์
* count 35, velocity 4, height 140 | ์ฐจ์ด 40
* count 40, velocity 2, height 80 | ์ฐจ์ด 60
* count 45, velocity 0, height 0 | ์ฐจ์ด 80
* */
const up = () => {
jumpCount++;
velocity = Math.max(velocity - Mario.GRAVITY, 0);
let nextBottom = jumpCount * velocity + this.defaultBottom;
this.element.style.bottom = nextBottom + 'px';
if (nextBottom > this.defaultBottom) requestAnimationFrame(up);
else this.isJumping = false;
};
up();
}
}
export default Mario;
๐ก ์ด๋ฏธ์ง ๋ฏธ๋ฆฌ ๋ก๋: ์๋ก์ด ์ด๋ฏธ์ง ์์๋ฅผ ๋ง๋ ๋ค src
์์ฑ์ ์ด๋ฏธ์ง URL๋ฅผ ํ ๋นํ๋ ์๊ฐ ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์ด๋ฏธ์ง๋ฅผ ๋ก๋ํ๊ธฐ ์์ํ๋ค. ๊ทธ ํ ๋ค๋ฅธ ์ด๋ฏธ์ง ์์์ src
์์ฑ์ ์ด๋ฏธ ๋ก๋ํ๋ ์ด๋ฏธ์ง URL์ ํ ๋นํ๋ฉด ๋ธ๋ผ์ฐ์ ๋ ์บ์์ ์ ์ฅ๋ ์ด๋ฏธ์ง๋ฅผ ์ฌ์ฉํ๋ค. ์ฐธ๊ณ ๋ก ํฌ๋กฌ ๋คํธ์ํฌ ํญ์์ ์บ์ ์ฌ์ฉ ์ค์ง ํญ๋ชฉ์ ๋นํ์ฑํํด์ผ ์บ์ ๊ธฐ๋ฅ์ด ์ ์์ ์ผ๋ก ์๋ํ๋ค — ๊ทธ ์ธ ๋ค๋ฅธ ๋ฐฉ๋ฒ์ ๋งํฌ ์ฐธ๊ณ
// mario.js
class Mario {
static JUMP_HEIGHT = 18; // ์ ํ ๋์ด. ๋์์๋ก ๋ ๋์ด ์ ํ
static GRAVITY = 0.4; // ์ค๋ ฅ ๊ฐ์๋. ๋ฎ์์๋ก ๋ ์ค๋ ์ ํ
audio;
defaultBottom;
isJumping = false;
// ...
constructor({ audio, defaultBottom, className = 'mario' }) {
this.audio = audio;
this.defaultBottom = defaultBottom;
// ...
}
jump() {
if (this.isJumping) return;
this.audio.playJumpSound();
this.isJumping = true;
let jumpCount = 0;
let velocity = Mario.JUMP_HEIGHT;
const up = () => {
jumpCount++;
velocity = Math.max(velocity - Mario.GRAVITY, 0);
let nextBottom = jumpCount * velocity + this.defaultBottom;
this.element.style.bottom = nextBottom + 'px';
if (nextBottom > this.defaultBottom) requestAnimationFrame(up);
else this.isJumping = false;
};
up();
}
// ...
}
์ฌ์ฉ์๊ฐ ์คํ์ด์ค๋ฐ๋ฅผ ๋๋ฅด๋ฉด jump()
๋ฉ์๋๋ฅผ ํธ์ถํ๋ค. ์ด ๋ฉ์๋๋ up()
ํจ์๋ฅผ ๋งค ํ๋ ์๋ง๋ค ํธ์ถํ์ฌ ์ ํ ๋์์ ๊ตฌํํ๋ค. up()
ํจ์์์ jumpCount
๋ฅผ 1์ฉ ์ฆ๊ฐ์ํค๊ณ , velocity
๋ฅผ Mario.GRAVITY
๋งํผ ๊ฐ์์ํจ๋ค.
์ดํ jumpCount
์ ๊ฐ์ํ velocity
๋ฅผ ๊ณฑํ๊ณ , ์ด ๊ฒฐ๊ณผ์ ๋ง๋ฆฌ์ค์ ๊ธฐ๋ณธ ๋์ด์ธ defaultBottom
์ ๋ํด์ ํ์ฌ ํ๋ ์์ ์ ํ ๋์ด์ธ nextBottom
์ ๊ณ์ฐํ๋ค. nextBottom
์ด defaultBottom
๋ณด๋ค ์๊ฑฐ๋ ๊ฐ์์ง๋ฉด ์ ํ๋ ์ข
๋ฃ๋๋ค.
velocity
๊ฐ ๋งค ํ๋ ์๋ง๋ค Mario.GRAVITY
๋งํผ ๊ฐ์ํ๋ฏ๋ก ์ ํ์ ํญ์ด ๋น ๋ฅด๊ฒ ์์นํ๋ค๊ฐ ์ ์ฐจ ์๋๊ฐ ๋๋ ค์ง๊ณ , ์ฆ๊ฐํ jumpCount
๋ก ์ธํด ์ ์ ์์ ๋ค์ ๋น ๋ฅด๊ฒ ํ๊ฐํ๋, ์ค๋ ฅ ํจ๊ณผ์ ์ ์ฌํ๊ฒ ์งํ๋๋ค.
์๋๋ ๋งค ํ๋ ์๋ง๋ค ๋์ด๊ฐ ์ด๋ป๊ฒ ๋ณํ๋์ง ๋ํ๋ธ ํ/์ด๋ฏธ์ง. ์ดํด๋ฅผ ๋๊ธฐ ์ํด velocity
๋ ๋งค ํ๋ ์๋ง๋ค 1์ฉ ๊ฐ์์ํค๊ณ defaultBottom
๊ฐ์ ๋์ด ๊ณ์ฐ์ ํฌํจํ์ง ์์๋ค.
์ ํ๋ฅผ ์์ํ ๋ ์์น ํญ์ด ๊ฐ์ฅ ๋๊ณ , ์ ์ ์ ๊ฐ๊น์์ง์๋ก ์ค๋ ฅ์ ์ํฅ์ผ๋ก ์์น ํญ์ด ์ ์ฐจ ๊ฐ์ํ์ฌ 0์ ๊ฐ๊น์์ง๋ค. ์ ์ ์ ์ง๋ ํ์๋ ์์ ์ผ๋ก ๋์๊ฐ๋๊น์ง ์์น ํญ์ด ๋ค์ ์ฆ๊ฐํ๋ค.
์นด์ดํธ | ์๋ | ๋์ด(์นด์ดํธ×์๋) | ์ด์ ๋์ด์ ์ฐจ์ด | ๊ตฌ๊ฐ |
0 | 16 | 0 | 0 | ์์ |
1 | 15 | 15 | 15 | ์์น ์์ |
2 | 14 | 28 | 13 | |
3 | 13 | 39 | 11 | |
4 | 12 | 48 | 9 | |
5 | 11 | 55 | 7 | |
6 | 10 | 60 | 5 | |
7 | 9 | 63 | 3 | |
8 | 8 | 64 | 1 | ์ ์ |
9 | 7 | 63 | 1 | ํ๊ฐ ์์ |
10 | 6 | 60 | 3 | ๋นจ๋ผ์ง๊ธฐ ์์ |
11 | 5 | 55 | 5 | |
12 | 4 | 48 | 7 | |
13 | 3 | 39 | 9 | |
14 | 2 | 28 | 11 | |
15 | 1 | 15 | 13 | |
16 | 0 | 0 | 15 | ์์ |
์ฅ์ ๋ฌผ ๋ฐฐ์น
์ฅ์ ๋ฌผ ์ด๋
// obstacle.js - Obstacle ํด๋์ค
class Obstacle {
constructor({ defaultBottom, speed, className = 'obstacle' } = {}) {
this.speed = speed;
this.point = 1;
this.frameId = null;
this.element = document.createElement('div');
this.element.classList.add(className);
this.element.style.bottom = defaultBottom + 'px';
this.element.style.right = '-100px'; // ์ฅ์ ๋ฌผ ์์ฑ์ ์์ฐ์ค๋ฝ๊ฒ ํ๊ธฐ ์ํด ์ด๊ธฐ๊ฐ์ ๋ ์ค๋ฅธ์ชฝ์ผ๋ก ์ง์
this.move = this.move.bind(this);
}
get isOutOfBounds() {
// ์ฅ์ ๋ฌผ์ด ๊ฒ์ ํ๋ฉด ์ผ์ชฝ ๊ฒฝ๊ณ๋ฅผ ๋ฒ์ด๋๋ฉด ์ ๊ฑฐ
// element.clientWidth ๊ฐ์ ์์์ ๋ด๋ถ ๋๋น(padding ํฌํจ)
return this.currentPosition >= DomManager.gameArea.clientWidth;
}
get currentPosition() {
return parseInt(this.element.style.right, 10); // parseInt('10.5px') => 10
}
move() {
const nextRight = this.currentPosition + this.speed;
this.element.style.right = nextRight + 'px'; // ์ฅ์ ๋ฌผ์ ์ผ์ชฝ์ผ๋ก ์ด๋
this.frameId = requestAnimationFrame(this.move);
}
stop() {
cancelAnimationFrame(this.frameId);
}
}
์ฅ์ ๋ฌผ์ด ๊ฐ์์ค๋ฝ๊ฒ ๋ํ๋๋ ๊ฒ์ ๋ฐฉ์งํ๊ธฐ ์ํด, ์ฅ์ ๋ฌผ์ ์ด๊ธฐ ์์น๋ฅผ ๊ฒ์ ํ๋ฉด์ ๋ณด์ด์ง ์๋ ์ค๋ฅธ์ชฝ์ ๋ฐฐ์นํ๋ค. ์ฅ์ ๋ฌผ ์์์ right
์คํ์ผ ์์ฑ์ ์์ ๊ฐ์ผ๋ก ์ง์ ํ๋ฉด ์ฅ์ ๋ฌผ์ด ํ๋ฉด ๋ฐ ์ค๋ฅธ์ชฝ์์ ์์ํ๊ฒ ๋๋ค.
๊ฒ์์ ์์ํ๋ฉด ๋งค ํ๋ ์๋ง๋ค ์ฅ์ ๋ฌผ์ right
์์ฑ์ speed
๊ฐ์ ์ผ์ ํ๊ฒ ๋ํด์ ์ผ์ชฝ์ผ๋ก ์ด๋์ํจ๋ค. ๊ทธ๋ผ ๋ง๋ฆฌ์ค๊ฐ ์์ผ๋ก ์ ์งํ๋ฉด์ ์ฅ์ ๋ฌผ๊ณผ ์ ์ ๊ฐ๊น์์ง๋ ํจ๊ณผ๋ฅผ ๊ตฌํํ ์ ์๋ค. ์ด๋ ๊ฒ์์ ๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง๊ฐ ์ํ ์ด๋ํ๋ speed
์ ์ฅ์ ๋ฌผ์ speed
๋ฅผ ๋์ผํ๊ฒ ์ค์ ํด์ ์ผ๊ด์ฑ ์๋ ์์ง์์ ๋ณด์ฅํ๋ ๊ฒ์ด ์ค์ํ๋ค.
// Obstacle ํด๋์ค (์ฅ์ ๋ฌผ ์ธ์คํด์ค๋ ์ด๋, ์ ์ง, ์์น ์ ๋ณด๋ฅผ ๋ฐํํ๋ ๊ธฐ๋ฅ์ ๊ฐ์ถ๋ค)
class Obstacle {
constructor({ defaultBottom, speed, className = 'obstacle' } = {}) {
this.element.style.right = '-100px'; // ์ฅ์ ๋ฌผ ์์ฑ์ ์์ฐ์ค๋ฝ๊ฒ ํ๊ธฐ ์ํด ์ด๊ธฐ ์์น๋ฅผ ๋ ์ค๋ฅธ์ชฝ์ผ๋ก ์ง์
this.move = this.move.bind(this);
// ...
}
get isOutOfBounds() {
return this.currentPosition >= DomManager.gameArea.clientWidth;
}
get currentPosition() {
return parseInt(this.element.style.right, 10); // parseInt('10.5px') => 10
}
move() {
const nextRight = this.currentPosition + this.speed;
this.element.style.right = nextRight + 'px'; // ์ฅ์ ๋ฌผ์ ์ผ์ชฝ์ผ๋ก ์ด๋
this.frameId = requestAnimationFrame(this.move);
}
// ...
}
element.clientWidth
์์ฑ์padding
์ ํฌํจํ ์๋ฆฌ๋จผํธ์ ๋ด๋ถ ๋๋น๋ฅผ ๋ฐํํ๋ค. ๋ง์ฝ ์ฅ์ ๋ฌผ ์์์right
์์ฑ ๊ฐ์ด ๊ฒ์ ํ๋ฉด์clientWidth
๋ณด๋ค ํฌ๊ฑฐ๋ ๊ฐ๋ค๋ฉด ํ๋ฉด์ ๋ฒ์ด๋ฌ์์ ์๋ฏธํ๋ค.parseInt
๋ฉ์๋ ์ธ์์ ๋๊ธด ๋ฌธ์์ด์ด ์ซ์๋ก ์์ํ๋ฉด ์ซ์(์ ์)๋ง ๋ฐํํ๋ค. ๋ฌธ์์ด์ด ์ซ์๋ก ์์ํ์ง ์์ ๋NaN
์ ๋ฐํํ๋ค. e.g.'100.5px'
→100
,'a100.5'
→NaN
์ฅ์ ๋ฌผ ๊ด๋ฆฌ
// obstacle.js - ObstacleManager ํด๋์ค
class ObstacleManager {
list = new Set();
frameId = null;
isMonitoring = false;
options;
constructor(options) {
this.options = options;
}
add() {
const obstacle = new Obstacle(this.options);
DomManager.gameArea.appendChild(obstacle.element);
this.list.add(obstacle);
obstacle.move();
}
remove(obstacle) {
obstacle.stop();
obstacle.element.remove();
this.list.delete(obstacle);
}
reset() {
this.list.forEach((obstacle) => this.remove(obstacle));
}
offScreenMonitor() {
const checkObstacles = () => {
this.list.forEach((obstacle) => {
if (obstacle.isOutOfBounds) this.remove(obstacle);
});
if (this.isMonitoring) {
this.frameId = requestAnimationFrame(checkObstacles);
}
};
checkObstacles();
}
moveAll() {
this.list.forEach((obstacle) => obstacle.move());
if (!this.isMonitoring) {
this.isMonitoring = true;
this.offScreenMonitor();
}
}
stopAll() {
this.list.forEach((obstacle) => obstacle.stop());
if (this.isMonitoring) {
cancelAnimationFrame(this.frameId);
this.isMonitoring = false;
}
}
}
export default ObstacleManager;
ObstacleManager
ํด๋์ค๋ ๊ฒ์ ๋ด์์ ์ฅ์ ๋ฌผ์ ์์ฑํ๊ณ ๊ด๋ฆฌํ๋ ํต์ฌ ์ญํ ์ ์ํํ๋ค. ์ฅ์ ๋ฌผ์ ์ค๋ณต ์์ด ๊ด๋ฆฌํ๊ธฐ ์ํด Set ๊ฐ์ฒด(list
ํ๋กํผํฐ)๋ฅผ ์ด์ฉํ์ฌ ๊ด๋ฆฌํ๋ค.
// ObstacleManager ํด๋์ค
class ObstacleManager {
list = new Set();
// ...
constructor(options) {
this.options = options;
}
add() {
const obstacle = new Obstacle(this.options);
DomManager.gameArea.appendChild(obstacle.element);
this.list.add(obstacle);
obstacle.move();
}
remove(obstacle) {
obstacle.stop();
obstacle.element.remove();
this.list.delete(obstacle);
}
offScreenMonitor() {
const checkObstacles = () => {
this.list.forEach((obstacle) => {
if (obstacle.isOutOfBounds) this.remove(obstacle);
});
if (this.isMonitoring) {
this.frameId = requestAnimationFrame(checkObstacles);
}
};
checkObstacles();
}
moveAll() {
this.list.forEach((obstacle) => obstacle.move());
if (!this.isMonitoring) {
this.isMonitoring = true;
this.offScreenMonitor();
}
}
// ...
}
moveAll()
๋ฉ์๋๋ ๋ชจ๋ ์ฅ์ ๋ฌผ์ ์ด๋ ๋ก์ง์ ์คํํ๊ณ , offScreenMonitor()
๋ฉ์๋๋ ๋งค ํ๋ ์๋ง๋ค ๊ฐ ์ฅ์ ๋ฌผ์ด ๊ฒ์ ์์ญ์ ๋ฒ์ด๋ฌ๋์ง ํ์ธํ๋ค. ๊ฒ์ ์์ญ์ ๋ฒ์ด๋ ์ฅ์ ๋ฌผ์ remove()
๋ฉ์๋๋ฅผ ํตํด ์ฆ์ ์ญ์ ๋๋ค. ์ฅ์ ๋ฌผ์ ์ถ๊ฐ/์ญ์ ๊ณผ์ ์์ ์๋ 3๊ฐ์ง๋ฅผ ๊ณ ๋ คํด์ผ ํ๋ค.
- ์ฅ์ ๋ฌผ ์ด๋ ํ๋ ์ ์์ ํน์ ์ค์ง
ObstacleManager
ํด๋์ค์list
๊ฐ์ฒด์ ์ถ๊ฐ ํน์ ์ญ์ - ๊ฒ์ ํ๋ฉด DOM์ ์ถ๊ฐ ํน์ ์ญ์
์ฅ์ ๋ฌผ ๋๋ค ์์ฑ
// game.js
class Game {
static DEFAULT_SPEED = 5;
static DEFAULT_BOTTOM = 50;
isPlaying = false;
obstacleTimerId = null;
collisionFrameId = null;
lastPassedObstacle = null;
audio;
score;
mario;
background;
obstacles;
eventHandler;
constructor({
speed = Game.DEFAULT_SPEED,
defaultBottom = Game.DEFAULT_BOTTOM,
} = {}) {
this.audio = new AudioManager();
this.score = new Score();
this.background = new Background({ speed });
this.obstacles = new ObstacleManager({ speed, defaultBottom });
this.mario = new Mario({ defaultBottom, audio: this.audio });
this.eventHandler = new EventHandler(this);
// ๋์ผํ ์ฐธ์กฐ์ ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ฅผ ์ฌ์ฉํด์ผ ์ด๋ฒคํธ๋ฅผ ์ ๊ฑฐํ ์ ์์ผ๋ฏ๋ก this.handleKeyDown ๋ฉ์๋ ๋ฐ์ธ๋ฉ
this.checkCollision = this.checkCollision.bind(this);
this.initializeController();
}
initializeController() {
DomManager.stopButton.onclick = () => this.stop();
DomManager.startButton.onclick = () => this.start();
DomManager.restartButton.onclick = () => this.restart();
DomManager.audioToggle.onclick = () => {
if (this.audio.isMuted) this.audio.unmute();
else this.audio.mute();
};
}
toggleButtonActive(shouldRestart) {
DomManager.startButton.disabled = shouldRestart;
DomManager.stopButton.disabled = shouldRestart;
DomManager.restartButton.disabled = !shouldRestart;
}
reset() {
this.obstacles.reset();
this.background.reset();
this.score.reset();
}
restart() {
this.reset();
this.start();
this.toggleButtonActive(false);
}
start() {
if (this.isPlaying) return;
this.isPlaying = true;
this.mario.run();
this.obstacles.moveAll();
this.background.move();
this.eventHandler.setupEventListeners();
this.checkCollision();
this.scheduleAddObstacle();
}
stop() {
if (!this.isPlaying) return;
this.isPlaying = false;
this.mario.stop();
this.obstacles.stopAll();
this.background.stop();
this.eventHandler.removeEventListeners();
cancelAnimationFrame(this.collisionFrameId);
clearInterval(this.obstacleTimerId);
}
failed() {
this.stop();
this.score.updateDialogScore();
DomManager.dialog.showModal();
}
scheduleAddObstacle() {
const randomInterval = generateRandomNumber(600, 1800);
this.obstacleTimerId = setTimeout(() => {
this.obstacles.add();
if (this.isPlaying) this.scheduleAddObstacle();
}, randomInterval);
}
checkCollision() {
const marioRect = this.mario.element.getBoundingClientRect();
for (let obstacle of this.obstacles.list) {
const obstacleRect = obstacle.element.getBoundingClientRect();
if (this.isColliding(marioRect, obstacleRect)) {
this.toggleButtonActive(true);
return this.failed();
}
if (this.isPassed(marioRect, obstacleRect)) {
this.lastPassedObstacle !== obstacle && this.score.add(obstacle.point);
this.lastPassedObstacle = obstacle;
}
}
this.collisionFrameId = requestAnimationFrame(this.checkCollision);
}
isColliding(marioRect, obstacleRect) {
/*
* ์ํ ์ถฉ๋ ์ํ
* Mario: |-----|
* Obstacle: |-----|
* */
const isHorizontalOverlap =
marioRect.left < obstacleRect.right && // ๋ง๋ฆฌ์ค๊ฐ ์ฅ์ ๋ฌผ ์ค๋ฅธ์ชฝ์์ ๊ฒน์น๋ ๊ฒฝ์ฐ ๊ฒ์ฌ
marioRect.right > obstacleRect.left; // ๋ง๋ฆฌ์ค๊ฐ ์ฅ์ ๋ฌผ ์ผ์ชฝ์์ ๊ฒน์น๋ ๊ฒฝ์ฐ ๊ฒ์ฌ
const isVerticalOverlap =
marioRect.top < obstacleRect.bottom && // ๋ง๋ฆฌ์ค๊ฐ ์ฅ์ ๋ฌผ ์๋์์ ๊ฒน์น๋ ๊ฒฝ์ฐ ๊ฒ์ฌ
marioRect.bottom > obstacleRect.top; // ๋ง๋ฆฌ์ค๊ฐ ์ฅ์ ๋ฌผ ์์์ ๊ฒน์น๋ ๊ฒฝ์ฐ ๊ฒ์ฌ
return isHorizontalOverlap && isVerticalOverlap;
}
isPassed(marioRect, obstacleRect) {
return marioRect.left > obstacleRect.right; // ๋ง๋ฆฌ์ค๊ฐ ์ฅ์ ๋ฌผ ๋์๋์ง ํ์ธ
}
}
export default Game;
์ฅ์ ๋ฌผ์ ๋ถ๊ท์นํ ์๊ฐ ๊ฐ๊ฒฉ์ผ๋ก ์์ฑํ๊ธฐ ์ํด setTimeout
ํ์ด๋จธ ๋ฉ์๋๋ฅผ ํ์ฉํ ์ ์๋ค. ๊ฒ์์ ์์ํ๋ฉด scheduleAddObstacle()
๋ฉ์๋๋ฅผ ํธ์ถํ๊ณ 600~1800 ์ฌ์ด์ ๋๋ค ์ซ์๋ฅผ ์์ฑํ๋ค. ์์ฑ๋ ๋๋ค ์ซ์๋ setTimeout
์ ๋ ๋ฒ์งธ ์ธ์์ธ delay
๋ก ์ค์ ํด์ ์ฅ์ ๋ฌผ ์ถ๊ฐ ํจ์ ์คํ์ ์ง์ฐ์ํจ๋ค.
์ง์ฐ ์๊ฐ์ด ๊ฒฝ๊ณผํ๋ฉด ์ฅ์ ๋ฌผ์ด ์ถ๊ฐ๋๊ณ , ๊ฒ์์ด ๊ณ์ ์งํ์ค์ธ ๊ฒฝ์ฐ scheduleAddObstacle()
๋ฉ์๋๋ฅผ ์ฌ๊ท์ ์ผ๋ก ํธ์ถํด์ ๋ค์ ์ฅ์ ๋ฌผ ์ถ๊ฐ๋ฅผ ์์ฝํ๋ ๊ณผ์ ์ด ๋ฐ๋ณต๋๋ค. ๊ฒ์ ๋์ด๋๋ฅผ ์ด๋ ต๊ฒ ๋ง๋ค๊ณ ์ถ๋ค๋ฉด ๋๋ค ์ซ์ ์์ฑ ๋ฒ์๋ฅผ ์ค์ฌ์ ์ฅ์ ๋ฌผ์ด ๋ ์์ฃผ ๋ํ๋๊ฒ ํ๋ฉด ๋๋ค.
// game.js
class Game {
static DEFAULT_SPEED = 5;
static DEFAULT_BOTTOM = 50;
obstacleTimerId = null;
obstacles;
// ...
constructor({
speed = Game.DEFAULT_SPEED,
defaultBottom = Game.DEFAULT_BOTTOM,
} = {}) {
this.obstacles = new ObstacleManager({ speed, defaultBottom });
// ...
}
start() {
// ...
this.scheduleAddObstacle();
}
scheduleAddObstacle() {
const randomInterval = generateRandomNumber(600, 1800);
this.obstacleTimerId = setTimeout(() => {
this.obstacles.add();
if (this.isPlaying) this.scheduleAddObstacle();
}, randomInterval);
}
// ...
}
์ฅ์ ๋ฌผ ์ถฉ๋ ๊ฐ์ง โญ
// game.js
class Game {
static DEFAULT_SPEED = 5;
static DEFAULT_BOTTOM = 50;
isPlaying = false;
obstacleTimerId = null;
collisionFrameId = null;
lastPassedObstacle = null;
audio;
score;
mario;
background;
obstacles;
eventHandler;
constructor({
speed = Game.DEFAULT_SPEED,
defaultBottom = Game.DEFAULT_BOTTOM,
} = {}) {
this.audio = new AudioManager();
this.score = new Score();
this.background = new Background({ speed });
this.obstacles = new ObstacleManager({ speed, defaultBottom });
this.mario = new Mario({ defaultBottom, audio: this.audio });
this.eventHandler = new EventHandler(this);
// ๋์ผํ ์ฐธ์กฐ์ ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ฅผ ์ฌ์ฉํด์ผ ์ด๋ฒคํธ๋ฅผ ์ ๊ฑฐํ ์ ์์ผ๋ฏ๋ก this.handleKeyDown ๋ฉ์๋ ๋ฐ์ธ๋ฉ
this.checkCollision = this.checkCollision.bind(this);
this.initializeController();
}
initializeController() {
DomManager.stopButton.onclick = () => this.stop();
DomManager.startButton.onclick = () => this.start();
DomManager.restartButton.onclick = () => this.restart();
DomManager.audioToggle.onclick = () => {
if (this.audio.isMuted) this.audio.unmute();
else this.audio.mute();
};
}
toggleButtonActive(shouldRestart) {
DomManager.startButton.disabled = shouldRestart;
DomManager.stopButton.disabled = shouldRestart;
DomManager.restartButton.disabled = !shouldRestart;
}
reset() {
this.obstacles.reset();
this.background.reset();
this.score.reset();
}
restart() {
this.reset();
this.start();
this.toggleButtonActive(false);
}
start() {
if (this.isPlaying) return;
this.isPlaying = true;
this.mario.run();
this.obstacles.moveAll();
this.background.move();
this.eventHandler.setupEventListeners();
this.checkCollision();
this.scheduleAddObstacle();
}
stop() {
if (!this.isPlaying) return;
this.isPlaying = false;
this.mario.stop();
this.obstacles.stopAll();
this.background.stop();
this.eventHandler.removeEventListeners();
cancelAnimationFrame(this.collisionFrameId);
clearInterval(this.obstacleTimerId);
}
failed() {
this.stop();
this.score.updateDialogScore();
DomManager.dialog.showModal();
}
scheduleAddObstacle() {
const randomInterval = generateRandomNumber(600, 1800);
this.obstacleTimerId = setTimeout(() => {
this.obstacles.add();
if (this.isPlaying) this.scheduleAddObstacle();
}, randomInterval);
}
checkCollision() {
const marioRect = this.mario.element.getBoundingClientRect();
for (let obstacle of this.obstacles.list) {
const obstacleRect = obstacle.element.getBoundingClientRect();
if (this.isColliding(marioRect, obstacleRect)) {
this.toggleButtonActive(true);
return this.failed();
}
if (this.isPassed(marioRect, obstacleRect)) {
this.lastPassedObstacle !== obstacle && this.score.add(obstacle.point);
this.lastPassedObstacle = obstacle;
}
}
this.collisionFrameId = requestAnimationFrame(this.checkCollision);
}
isColliding(marioRect, obstacleRect) {
/*
* ์ํ ์ถฉ๋ ์ํ
* Mario: |-----|
* Obstacle: |-----|
* */
const isHorizontalOverlap =
marioRect.left < obstacleRect.right && // ๋ง๋ฆฌ์ค๊ฐ ์ฅ์ ๋ฌผ ์ค๋ฅธ์ชฝ์์ ๊ฒน์น๋ ๊ฒฝ์ฐ ๊ฒ์ฌ
marioRect.right > obstacleRect.left; // ๋ง๋ฆฌ์ค๊ฐ ์ฅ์ ๋ฌผ ์ผ์ชฝ์์ ๊ฒน์น๋ ๊ฒฝ์ฐ ๊ฒ์ฌ
const isVerticalOverlap =
marioRect.top < obstacleRect.bottom && // ๋ง๋ฆฌ์ค๊ฐ ์ฅ์ ๋ฌผ ์๋์์ ๊ฒน์น๋ ๊ฒฝ์ฐ ๊ฒ์ฌ
marioRect.bottom > obstacleRect.top; // ๋ง๋ฆฌ์ค๊ฐ ์ฅ์ ๋ฌผ ์์์ ๊ฒน์น๋ ๊ฒฝ์ฐ ๊ฒ์ฌ
return isHorizontalOverlap && isVerticalOverlap;
}
isPassed(marioRect, obstacleRect) {
return marioRect.left > obstacleRect.right; // ๋ง๋ฆฌ์ค๊ฐ ์ฅ์ ๋ฌผ ๋์๋์ง ํ์ธ
}
}
export default Game;
๐ก DOM์ ๋ณ๊ฒฝ์ฌํญ์ด ๋ฐ์ํ์ง๋ง ์์ง ํ๋ฉด์ ๋ฐ์ํ์ง ์์ ์ํ๋ผ๋ฉด getBoundingClientRect()
๋ฉ์๋๋ฅผ ํธ์ถํ์ ๋ ๋ฆฌํ๋ก์ฐ(๋ ์ด์์ ๋ค์ ๊ณ์ฐ)๊ฐ ๋ฐ์ํ๋ค. ๋ธ๋ผ์ฐ์ ๊ฐ ์์์ ์ ํํ ์์น์ ํฌ๊ธฐ ์ ๋ณด๋ฅผ ์ ๊ณตํ๊ธฐ ์ํด ๋ ์ด์์์ ๊ณ์ฐํด์ผ ํ๊ธฐ ๋๋ฌธ์ด๋ค. ๋ง์ฝ DOM์ ๋ณ๊ฒฝ์ฌํญ์ด ์๊ณ ๋ธ๋ผ์ฐ์ ๊ฐ ์ต์ ๋ ์ด์์ ์ ๋ณด๋ฅผ ์ด๋ฏธ ๊ณ์ฐํด๋ ์ํ๋ผ๋ฉด ๋ฆฌํ๋ก์ฐ๊ฐ ๋ฐ์ํ์ง ์๋๋ค.
๊ฒ์์ ์์ํ๋ฉด checkCollision()
๋ฉ์๋๋ฅผ ํธ์ถํด์ ๋งค ํ๋ ์๋ง๋ค ๋ชจ๋ ์ฅ์ ๋ฌผ ์์์ ๋ํด ๋ง๋ฆฌ์ค์ ์ถฉ๋ ์ฌ๋ถ๋ฅผ ๊ฒ์ฌํ๋ค. ๋ง๋ฆฌ์ค์ ์ฅ์ ๋ฌผ์ด ์ถฉ๋ํ๋ค๋ฉด ๊ฒ์์ ์ค์ง๋๋ค.
๊ฐ ์์์ ์์น๋ ๋ทฐํฌํธ ๊ธฐ์ค์ ์ขํ ๊ฐ์ ๋ฐํํ๋ element.getBoundingClientRect()
๋ฉ์๋๋ฅผ ํธ์ถํด์ ํ๋ํ ์ ์๋ค. — ๋ ์์ธํ ๋ด์ฉ์ ์ด์ ํฌ์คํ
์ฐธ๊ณ
// game.js
class Game {
// ...
collisionFrameId = null;
constructor({
speed = Game.DEFAULT_SPEED,
defaultBottom = Game.DEFAULT_BOTTOM,
} = {}) {
this.obstacles = new ObstacleManager({ speed, defaultBottom });
this.mario = new Mario({ defaultBottom, audio: this.audio });
// ...
}
start() {
// ...
this.checkCollision();
}
checkCollision() {
const marioRect = this.mario.element.getBoundingClientRect();
for (let obstacle of this.obstacles.list) {
const obstacleRect = obstacle.element.getBoundingClientRect();
if (this.isColliding(marioRect, obstacleRect)) {
this.toggleButtonActive(true);
return this.failed();
}
// ...
}
this.collisionFrameId = requestAnimationFrame(this.checkCollision);
}
isColliding(marioRect, obstacleRect) {
const isHorizontalOverlap =
marioRect.left < obstacleRect.right && // ๋ง๋ฆฌ์ค๊ฐ ์ฅ์ ๋ฌผ ์ค๋ฅธ์ชฝ์์ ๊ฒน์น๋ ๊ฒฝ์ฐ ๊ฒ์ฌ
marioRect.right > obstacleRect.left; // ๋ง๋ฆฌ์ค๊ฐ ์ฅ์ ๋ฌผ ์ผ์ชฝ์์ ๊ฒน์น๋ ๊ฒฝ์ฐ ๊ฒ์ฌ
const isVerticalOverlap =
marioRect.top < obstacleRect.bottom && // ๋ง๋ฆฌ์ค๊ฐ ์ฅ์ ๋ฌผ ์๋์์ ๊ฒน์น๋ ๊ฒฝ์ฐ ๊ฒ์ฌ
marioRect.bottom > obstacleRect.top; // ๋ง๋ฆฌ์ค๊ฐ ์ฅ์ ๋ฌผ ์์์ ๊ฒน์น๋ ๊ฒฝ์ฐ ๊ฒ์ฌ
return isHorizontalOverlap && isVerticalOverlap;
}
// ...
}
checkCollision()
๋ฉ์๋๋ ๊ฐ ์ฅ์ ๋ฌผ์ ์ํํ๋ฉด์ ๋ง๋ฆฌ์ค์ ์ฅ์ ๋ฌผ์ ํ์ฌ ์ขํ๋ฅผ ํ๋ํ๋ค. ๋ง๋ฆฌ์ค์ ์ขํ ๊ฐ์ ๋งค๋ฒ ํ์ธํ๋ ์ด์ ๋ ๋ง๋ฆฌ์ค๊ฐ ์ ํ ์ค์ผ ๋ ๋งค ํ๋ ์๋ง๋ค ์์น๊ฐ ๊ณ์ ๋ณํ๊ธฐ ๋๋ฌธ์ด๋ค. ์ด ์ขํ๋ค์ isColliding()
๋ฉ์๋์ ์ ๋ฌ๋ผ์ ์ํ๊ณผ ์์ง ์ถฉ๋ ์ฌ๋ถ๋ฅผ ๊ฒ์ฌํ๋ ๋ฐ ์ฌ์ฉ๋๋ค(์๋ ์ด๋ฏธ์ง ์ฐธ๊ณ ).
- ์ํ ์ถฉ๋ Horizontal Overlap
- ๋ง๋ฆฌ์ค ์ข์ธก ๋ณ์ด ์ฅ์ ๋ฌผ ์ฐ์ธก ๋ณ๋ณด๋ค ์ผ์ชฝ์ ์๊ณ ,
- ๋ง๋ฆฌ์ค ์ฐ์ธก ๋ณ์ด ์ฅ์ ๋ฌผ ์ข์ธก ๋ณ๋ณด๋ค ์ค๋ฅธ์ชฝ์ ์์ ๋
- ์์ง ์ถฉ๋ Vertical Overlap
- ๋ง๋ฆฌ์ค ์๋จ ๋ณ์ด ์ฅ์ ๋ฌผ ํ๋จ ๋ณ๋ณด๋ค ์์ ์๊ณ ,
- ๋ง๋ฆฌ์ค ํ๋จ ๋ณ์ด ์ฅ์ ๋ฌผ ์๋จ ๋ณ๋ณด๋ค ์๋์ ์์ ๋
์์ฑ ๋ ํฌ์งํ ๋ฆฌ
GitHub - romantech/super-mario: Super Mario Runner with Vanilla JavaScript
Super Mario Runner with Vanilla JavaScript. Contribute to romantech/super-mario development by creating an account on GitHub.
github.com
๊ธ ์์ ์ฌํญ์ ๋ ธ์ ํ์ด์ง์ ๊ฐ์ฅ ๋น ๋ฅด๊ฒ ๋ฐ์๋ฉ๋๋ค. ๋งํฌ๋ฅผ ์ฐธ๊ณ ํด ์ฃผ์ธ์
'๐ช Programming' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[DevTools] ์น์์ VSCode ์ฌ์ฉํ๊ธฐ - github.dev / github1s (0) | 2024.05.29 |
---|---|
[AWS] VSCode์์ AWS EC2 ์๊ฒฉ ์ฐ๊ฒฐ / EC2 ๋ฉ๋ชจ๋ฆฌ ๋ถ์กฑ ํด๊ฒฐ (0) | 2024.05.29 |
[HTML] select, option ํ๊ทธ ์ฃผ์ ์์ฑ๊ณผ ํน์ง (0) | 2024.05.29 |
[Algorithm] ์์ด / ์กฐํฉ ๊ฐ๋ ๊ณผ ์๊ณ ๋ฆฌ์ฆ ๊ตฌํ (0) | 2024.05.28 |
[React] ๋ฆฌ์กํธ์์ setTimeout ๋ ํธํ๊ฒ ์ฐ๊ธฐ (0) | 2024.05.28 |
๋๊ธ
์ด ๊ธ ๊ณต์ ํ๊ธฐ
-
๊ตฌ๋
ํ๊ธฐ
๊ตฌ๋ ํ๊ธฐ
-
์นด์นด์คํก
์นด์นด์คํก
-
๋ผ์ธ
๋ผ์ธ
-
ํธ์ํฐ
ํธ์ํฐ
-
Facebook
Facebook
-
์นด์นด์ค์คํ ๋ฆฌ
์นด์นด์ค์คํ ๋ฆฌ
-
๋ฐด๋
๋ฐด๋
-
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
-
Pocket
Pocket
-
Evernote
Evernote
๋ค๋ฅธ ๊ธ
-
[DevTools] ์น์์ VSCode ์ฌ์ฉํ๊ธฐ - github.dev / github1s
[DevTools] ์น์์ VSCode ์ฌ์ฉํ๊ธฐ - github.dev / github1s
2024.05.29 -
[AWS] VSCode์์ AWS EC2 ์๊ฒฉ ์ฐ๊ฒฐ / EC2 ๋ฉ๋ชจ๋ฆฌ ๋ถ์กฑ ํด๊ฒฐ
[AWS] VSCode์์ AWS EC2 ์๊ฒฉ ์ฐ๊ฒฐ / EC2 ๋ฉ๋ชจ๋ฆฌ ๋ถ์กฑ ํด๊ฒฐ
2024.05.29 -
[HTML] select, option ํ๊ทธ ์ฃผ์ ์์ฑ๊ณผ ํน์ง
[HTML] select, option ํ๊ทธ ์ฃผ์ ์์ฑ๊ณผ ํน์ง
2024.05.29 -
[Algorithm] ์์ด / ์กฐํฉ ๊ฐ๋ ๊ณผ ์๊ณ ๋ฆฌ์ฆ ๊ตฌํ
[Algorithm] ์์ด / ์กฐํฉ ๊ฐ๋ ๊ณผ ์๊ณ ๋ฆฌ์ฆ ๊ตฌํ
2024.05.28