๋ฐ˜์‘ํ˜•

 

๋Ÿฌ๋„ˆ ๊ฒŒ์ž„(๋‹ฌ๋ฆฌ๊ธฐ ๊ฒŒ์ž„)์€ ํ”Œ๋ ˆ์ด์–ด๊ฐ€ ์ž๋™์œผ๋กœ ์ „์ง„ํ•˜๋ฉด์„œ ์žฅ์• ๋ฌผ์„ ํšŒํ”ผํ•˜๊ณ , ์ ์ˆ˜๋ฅผ ํš๋“ํ•˜๋Š” ๊ฒŒ์ž„ ์œ ํ˜•์ด๋‹ค. ๊ฐ„๋‹จํ•œ ๋Ÿฌ๋„ˆ ๊ฒŒ์ž„์€ ์บ”๋ฒ„์Šค ์—†์ด 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๊ฐ€์ง€๋ฅผ ๊ณ ๋ คํ•ด์•ผ ํ•œ๋‹ค.

 

  1. ์žฅ์• ๋ฌผ ์ด๋™ ํ”„๋ ˆ์ž„ ์‹œ์ž‘ ํ˜น์€ ์ค‘์ง€
  2. ObstacleManager ํด๋ž˜์Šค์˜ list ๊ฐ์ฒด์— ์ถ”๊ฐ€ ํ˜น์€ ์‚ญ์ œ
  3. ๊ฒŒ์ž„ ํ™”๋ฉด 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() ๋ฉ”์„œ๋“œ์— ์ „๋‹ฌ๋ผ์„œ ์ˆ˜ํ‰๊ณผ ์ˆ˜์ง ์ถฉ๋Œ ์—ฌ๋ถ€๋ฅผ ๊ฒ€์‚ฌํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋œ๋‹ค(์•„๋ž˜ ์ด๋ฏธ์ง€ ์ฐธ๊ณ ).

 

Axis-Aligned Bounding Box Collision Detection (isColliding ๋ฉ”์„œ๋“œ ๋กœ์ง)

  1. ์ˆ˜ํ‰ ์ถฉ๋Œ Horizontal Overlap
    • ๋งˆ๋ฆฌ์˜ค ์ขŒ์ธก ๋ณ€์ด ์žฅ์• ๋ฌผ ์šฐ์ธก ๋ณ€๋ณด๋‹ค ์™ผ์ชฝ์— ์žˆ๊ณ ,
    • ๋งˆ๋ฆฌ์˜ค ์šฐ์ธก ๋ณ€์ด ์žฅ์• ๋ฌผ ์ขŒ์ธก ๋ณ€๋ณด๋‹ค ์˜ค๋ฅธ์ชฝ์— ์žˆ์„ ๋•Œ
  2. ์ˆ˜์ง ์ถฉ๋Œ 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

 

 


๊ธ€ ์ˆ˜์ •์‚ฌํ•ญ์€ ๋…ธ์…˜ ํŽ˜์ด์ง€์— ๊ฐ€์žฅ ๋น ๋ฅด๊ฒŒ ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค. ๋งํฌ๋ฅผ ์ฐธ๊ณ ํ•ด ์ฃผ์„ธ์š”
๋ฐ˜์‘ํ˜•