프로그래밍/JavaScript

[JS, Phaser 3] 자바스크립트로 게임 개발하기 | 뱀파이어 서바이벌 클론코딩

choar 2022. 6. 7. 14:04
반응형

[JS, Phaser 3] 자바스크립트로 게임 개발하기 | 뱀파이어 서바이벌 클론코딩

 

 

2022 제주 웹 컨퍼런스에서 발표한 내용을 포스팅으로 정리합니다.

발표 자료는 해당 링크에서 확인하실 수 있습니다.

해당 컨퍼런스는 2022년 6월 2일~4일 동안 진행되었습니다.

저는 'JavaScript로 게임 만들어서 부자되기'라는 세션을 다음과 같은 내용으로 진행했습니다.

  • 개발로 부수입 창출하기
  • Phaser 3 소개 및 주요 개념
  • 뱀파이어 서바이벌(Vampire Survivors) 클론코딩

앞의 내용은 생략하고, 뱀파이어 서바이벌 클론코딩에 대해 자세히 정리해보려고 합니다.

 

 

 

뱀파이어 서바이벌 클론코딩

 

일단 제가 클론코딩해서 개발한 Meow Meow Fuzzyface를 한번 플레이해보고 오시면 좋을 것 같습니다.

해보면 아시겠지만 Level up시 무기 추가하는 기능, Level 4 이후로 Enemy 추가하는 기능 등등 구현하지 못한 부분이 좀 있습니다.. 😉

세션에서 진행할 클론코딩(meow-hands-on)은 더 간단하게 만들었는데, 반도 진행하지 못했습니다.. 시간이 정말 빨리 가더군요 🥺

 

뱀파이어 서바이벌은 재미있고 인기도 많은 게임 중 하나인데, JavaScript와 Phaser 3로 개발되었습니다.

Phaser 3JavaScript로 2D 게임을 만들 수 있는 프레임워크, 즉 게임 엔진입니다.

Phaser 3의 주요 개념으로는 다음과 같은 것들이 있습니다.

여기서 제일 중요한 개념은 Scene입니다.

Phaser에서는 Scene 단위로 개발을 하게 됩니다.

Meow Meow Fuzzyface를 예시로 들면, 시작 전 화면 / 플레이 화면 / 게임 오버 화면으로 나누어 개발했습니다.

Scene에는 생명주기가 있습니다.

React class component나 Android에 익숙한 분들이라면 생명주기가 무엇인지 익숙하실 텐데요,

Scene이 생성되고 사라지기까지 순차적으로 실행되는 함수를 의미합니다.

여기서 중요한 것은 update 함수는 Scene이 실행될 때 매 프레임마다 호출된다는 것입니다.

Phaser Scene의 생명주기 전체는 해당 링크에서 확인하실 수 있습니다.

 

자, 그렇다면 이제 본격적으로 클론코딩을 시작해봅시다!

 

1. 틀 다운로드 받기

일단 여러분들을 위해 틀을 만들어 놓았으니 터미널에서 다음 명령어를 입력해 틀을 다운로드 받아주세요.

git clone https://github.com/choar816/meow-template.git
cd meow-template
code . (또는 IDE로 meow-template 폴더 열기)
npm install
npm start

npm start를 입력하시면 다음과 같이 까만 화면과 'Loading game...'이라는 문구를 확인할 수 있습니다!

템플릿에서 주요 코드를 살펴보겠습니다.

// src/scenes/LoadingScene.js
import Phaser from 'phaser';
import fontPng from "../assets/font/font.png";
import fontXml from "../assets/font/font.xml";
import bgImg from '../assets/images/background.png';
import catImg from '../assets/images/cat.png';
import beamImg from '../assets/images/beam.png';
import fireOgg from "../assets/sounds/fire.ogg";
import batImg from "../assets/spritesheets/bat.png";

// Phaser.Scene을 상속하여 LoadingScene 클래스를 정의해 줌
export default class LoadingScene extends Phaser.Scene {
    constructor() {
        super("bootGame");
        // bootGame : 이 scene의 identifier
    }

    preload() {
    	// 이미지를 로드해 줌
        this.load.image("background", bgImg);
        this.load.image("cat", catImg);
        this.load.image("beam", beamImg);

		// sprite image를 로드하고, 한 칸의 크기 정의
        this.load.spritesheet("bat", batImg, {
            frameWidth: 16,
            frameHeight: 16
        });
        // .. 생략
        
        // 폰트를 로드해 줌
        this.load.bitmapFont("pixelFont", fontPng, fontXml);
        
        // audio를 로드해 줌
        this.load.audio("audio_beam", fireOgg);
        // .. 생략
    }

    create() {
    	// 화면에 text 추가
        this.add.text(20, 20, "Loading game...");

		// sprite image에서 animation 정의
        this.anims.create({
            key: "bat_anim",
            frames: this.anims.generateFrameNumbers("bat"),
            frameRate: 12,
            repeat: -1
        });
        // .. 생략
    }
}
// src/Config.js
import LoadingScene from "./scenes/LoadingScene";
// import MainScene from "./scenes/MainScene";
// import PlayingScene from "./scenes/PlayingScene";
// import GameoverScene from "./scenes/GameoverScene";

const Config = {
    // 맵 크기
    width: 800,
    height: 600,
    backgroundColor: 0x000000,
    scene: [LoadingScene], // 사용할 scene들은 해당 배열에 넣어줘야 함
    pixelArt: true, // pixelArt를 사용할 시 true로 해야 이미지가 선명하게 나옴
    physics: {
        default: "arcade", // arcade라는 물리 엔진을 사용할 것임
        arcade: {
            debug: process.env.DEBUG === "true",
        },
    },
};

export default Config;
// src/index.js
import Config from "./Config";

const game = new Phaser.Game(Config);

export default game;

 

2. Scene 추가하기

게임 asset들을 로딩하는 Scene 말고, 게임을 플레이하는 Scene도 있어야겠죠?

사실 한 Scene에서 두 작업을 모두 수행해도 되지만 그러면 코드가 너무 길어져 개발하기 힘드실 겁니다.

따라서 다음 커밋과 같이 PlayingScene을 추가해주세요!

 

오디오를 add한 부분이 눈에 띄실 텐데요,

load는 전역적으로 어떤 Scene에서든 asset을 사용할 수 있도록 load 해주는 것이고,

add는 해당 Scene에서 사용할 수 있도록 멤버 변수로 추가할 때 사용하는 것으로 생각해주시면 됩니다.

background는 tileSprite라는 것으로 추가했는데요,

이는 map size가 background image보다 커질 시 타일처럼 다닥다닥 붙여서 보여주는 이미지입니다. (CSS의 background-repeat: repeat; 같은 느낌)

 

3. Player를 추가해보자

다음 커밋을 보고 따라 진행해주시면 됩니다.

Player 클래스를 만들어주었습니다.

scene.add.existing 함수는 해당 scene에 오브젝트를 추가하는 함수이고,

scene.physics.add.existing 함수는 해당 scene에 추가한 오브젝트를 물리 엔진에 적용시키는 함수입니다.

 

4. Player가 움직일 수 있도록 만들어보자

다음 커밋을 보고 따라 진행해주시면 됩니다.

보시다시피 코드가 좀 지저분한데.. 😂 부드러운 움직임을 구현하려고 열심히 구글링과 머리를 짜낸 결과입니다.

더 좋은 아이디어가 있으신 분은 댓글 남겨주세요!

 

5. 무한 배경을 구현해보자

다음 커밋을 보고 따라 진행해주시면 됩니다.

뱀파이어 서바이벌을 생각해보면, 일단 Player가 화면 가운데 고정되어야겠죠?

다음 코드가 바로 그 역할을 하게 됩니다.

// src/scenes/PlayingScene.js
this.cameras.main.startFollow(this.m_player);

무한 배경을 구현하기 위해 tileSprite를 매우 큰 크기로 설정하는 것은 시간도 많이 걸리고, 진짜 '무한'이 아니겠죠?

그래서 Player를 따라 map size에 딱 맞는 배경이 따라오도록 하고, 그 배경이 보여주는 tileSprite가 Player가 이동한만큼 움직이도록 구현했습니다.

주석을 참고해주세요 🙂

 

6. Player가 Attack을 할 수 있도록 만들자

다음 커밋을 보고 따라 진행해주시면 됩니다.

Beam 클래스를 생성했고,

일정 시간 간격으로 반복되는 event는 scene.time.addEvent를 활용했습니다.

6번까지 진행한 모습

7. Enemy를 만들어보자

다음 커밋 보고 따라 진행해주시면 됩니다.

Enemy 클래스를 만들어줍니다.

Enemy는 항상 Player를 향해 움직여야 하는데, 이는 physics.moveToObject라는 메소드를 사용하시면 됩니다.

또한, Enemy와 Player가 접촉(overlap)하면 Player의 HP가 깎여야 합니다.

Enemy와 Beam이 접촉하면 Enemy의 HP가 Beam의 공격력만큼 감소되어야겠죠?

이런 기능들을 클래스에 메소드를 생성하고, 물리 엔진을 활용해 만들어줍니다.

마지막으로 Enemy를 PlayingScene에 add 해주면 짠! 생성되었습니다.

7번까지 진행한 모습

 

8. Enemy를 많이 만들어보자

다음 커밋 보고 따라 진행해주시면 됩니다.

6번처럼 scene.time.addEvent를 사용해 1초마다 Enemy가 생성되도록 만듭니다.

그런데 지금 보는 화면에 Enemy가 갑자기 나타난다면 이상하겠죠?

저는 그래서 Player로부터 r(map의 대각선의 절반)만큼 거리가 떨어진 곳에서 Enemy가 생성되도록 했습니다.

이렇게 하면 자연스럽게 Enemy가 생기도록 할 수 있습니다.

 

9. Attack이 가장 가까운 Enemy를 향하도록 만들어보자

다음 커밋 보고 따라 진행해주시면 됩니다.

일단 Attack이 위로만 가도록 만들었었는데, 제일 가까운 Enemy를 향하도록 만들어봅시다.

physics.closest를 이용하면 간단히 가장 가까운 Enemy를 알아낼 수 있습니다.

Enemy class에서 velocity 설정하는 함수도 만들어줍니다.

 

10. Player의 HP bar를 만들어보자

다음 커밋 보고 따라 진행해주시면 됩니다.

HpBar class를 만들어주시고, Phaser.GameObjects.Graphics의 메서드를 사용해 HP bar를 그려줍니다.

Player가 Enemy와 닿으면 HP가 감소하고, 1초 동안 다시 공격받지 않도록 쿨타임을 만들어 줍니다.

이때 disableBody 메소드를 활용합니다.

HP bar

 

11. Main Scene을 추가해보자

다음 커밋 보고 따라 진행해주시면 됩니다.

게임이 바로 시작돼서 불편하셨죠? Main Scene을 만들어줍니다.

이때, Button class를 만들어놓으면 재사용할 수 있어 편리합니다.

Main Scene

12. 처치한 Enemy 수를 추가해보자

다음 커밋 보고 따라 진행해주시면 됩니다.

게임 화면 상단에 처치한 Enemy 수를 추가해줍니다.

Enemy killed가 게임 화면, 오브젝트보다 위에 뜨게 하기 위해 setDepth를 사용합니다.

CSS에서 z-index 설정하는 것과 비슷하다고 생각하시면 됩니다.

또한 setScrollFactor(0)은 CSS의 position: fixed;와 같다고 보시면 됩니다.

Enemy killed

 

13. Game Over Scene을 추가해보자

다음 커밋 보고 따라 진행해주시면 됩니다.

GameoverScene class를 만들고, Player의 HP가 0이 되면 GameoverScene을 띄워줍니다.

이때 Enemy killed를 함께 띄우기 위해 PlayingScene으로부터 정보를 받아옵니다.

Game Over Scene

 

와!! 드디어 제가 준비한 뱀파이어 서바이얼 클론코딩이 완료되었습니다!! 👏

뱀파이어 서바이얼의 간단 버전의 간단 버전을 직접 만들어 보니까 어떠셨나요?

저는 생각보다 어려웠어요. 결과물이 생각보다 단순해졌지만 그래도 내 손으로 만든 게임이라 정이 갑니다 ㅎㅎ

제 포스팅이 누군가에게 조금이라도 도움이나 재미를 주었다면 좋겠네요 🙂

개발 소감

 

 

첫 컨퍼런스 연사 소감

 

일단 좋은 기회 주신 위니브 측에 다시 한번 감사의 말씀드립니다. 🙇

제가 한다고 했지만 취준생이고 아무것도 모르는 제가 연사를 해도 되나 싶어서 걱정을 많이 했습니다.

하지만 걱정한 만큼 열심히 준비해서 공부도 많이 됐고, 참여자분들께도 새로운 지식을 전달드릴 수 있어 보람찼습니다.

앞으로도 공부 열심히 해서 이런 자리에 많이 참여하면 좋겠다는 생각을 했습니다!!

 

혹시 해당 포스팅을 따라 클론코딩을 진행하면서 질문이 생기신 분들은 언제든 댓글 남겨주세요 🙂

 


References

Phaser 3 공식 문서 : https://photonstorm.github.io/phaser3-docs/

Phaser 3 Tutorial (Youtube) : https://www.youtube.com/playlist?list=PLDyH9Tk5ZdFzEu_izyqgPFtHJJXkc79no

Phaser 3 Tutorial (GitHub) : https://github.com/ansimuz/getting-started-with-phaser

Phaser 3 Examples : http://phaser.io/examples/v3

Phaser 3 Examples : https://github.com/photonstorm/phaser#create-your-first-phaser-3-example

반응형