상세 컨텐츠

본문 제목

프로젝트#1. 로그라이크 게임개발 JavaScript(2)

내일배움캠프 학습/진행 프로젝트

by 남민우_ 2024. 11. 17. 23:02

본문

지난 글에서는 Battle.js 파일까지 설명을 마쳤다.

이번 글에서는 그 이후의 동작과 코드들에 대해서 설명한다.

Ch5. SpawnMonster.js

import {Monster} from './Monster.js';
import { BossMonster } from './BossMonster.js';
import fs from 'fs';

function loadMonsters(currentStage)
{
    const data = fs.readFileSync('monster.json', 'utf-8');
    const monsters = JSON.parse(data);    

    return monsters.find(monster => monster.stage === currentStage);
}

export function spawnMonster(currentStage)
{
    const loadMemory = loadMonsters(currentStage);
    const monster = loadMemory.type === "Normal" ? 
         new Monster(loadMemory.name, loadMemory.hp, loadMemory.atk, loadMemory.exp, loadMemory.type)
        : new BossMonster(loadMemory.name, loadMemory.hp, loadMemory.atk, loadMemory.exp, loadMemory.type, loadMemory.def);
    
    return monster;
}

 

간단히 말해서 몬스터 객체를 생성하는 기능을 담당하는 파일이다.

loadMonsters(currentStage)

다음과 같이 구성된 monster.json 파일이 있는데 이를 불러와 currentStage, 즉 현재 스테이지에 맞는 몬스터를 find 함수를 통해 찾고 이를 반환한다.

spawnMonster(currentStage)

이전 글에서 설명했듯 battle 함수에서 호출되며, 현재 스테이지를 매개변수로 받는다.

loadMonsters 함수를 통해 스테이지에 맞는 몬스터를 불러오고 이를 삼항연산자를 통해 보스 몬스터인지, 일반 몬스터인지 판단 후 그에 맞는 몬스터 객체를 생성 후 반환한다.

따라서 battle 함수 내부에서는 이 함수 호출에 현재 스테이지 값을 입력하는 것만으로 현재 상황에 맞는 몬스터를 생성할 수 있도록 하였다.

이 사진 상태는 battle 함수와 이 spawnMonster 함수까지 연계된 동작을 나타낸다.

현재 스테이지가 1이므로 displayStatus 를 통해 일반 스테이지 1 과 플레이어 정보, 일반 몬스터 정보를 보여준다.

만약 스테이지 1을 클리어하고 다음 스테이지로 나아갈 경우

다음과 같이 새 몬스터 객체 생성과 displayStatus 갱신 및 특수 선택지 3번 패링이 나타난다.

1번 2번은 기존과 동일하게 작동하되, 3번 패링을 고를 경우

다음과 같이 선택지가 나타나며 그 중 하나를 골라 진행하면 

패링의 결과에 따라 후처리가 진행된다.

그러면 이 생성되는 몬스터들의 코드들을 살펴보자.

 

Ch6. monster.js / BossMonster.js

import chalk from 'chalk';

export class Monster {
    constructor(name, hp, atk, exp, type) 
    {
        this._NAME = name;
        this._HP = hp;
        this._ATK = atk;
        this._EXP = exp;
        this._TYPE = type;
    }
// ----------------------------------------------------------------------
    Mon_Attack()
    {
        return {
            damage : this._ATK,
            message : chalk.red(`몬스터가 ${this._ATK} 의 공격을 합니다.`)
        }
    }
    Mon_Hitted(hit_damage)
    {
        this._HP -= hit_damage;
        return chalk.blue(`몬스터가 ${hit_damage} 의 피해를 입었습니다.`);
    }

// ----------------------------------------------------------------------
    get NAME() { return this._NAME; }
    get HP() { return this._HP; }
    get ATK() { return this._ATK; }
    get EXP() { return this._EXP; }
    get TYPE() { return this._TYPE; }

    set NAME(value)
    {
        if(typeof value !== 'string')
        {
            console.log(`${this._NAME} : Type Error`);
            process.exit(0);
        }
        this._NAME = value;
    }
    set HP(value) { this._HP = value; }
}

 

먼저 일반 몬스터의 클래스이다.

생성자에서 몬스터에게 필요한 수치, 메서드에서 공격과 피격을 처리하며 getter 와 setter를 통해 수치를 처리할 수 있도록 구성하였다.

사실 일반 몬스터는 기능 자체가 매우 단순하여 코드가 어렵지 않은 편이다.

 

import { Monster } from "./Monster.js"
import chalk from 'chalk';

export class BossMonster extends Monster
{
    constructor(name, hp, atk, exp, type, def)
    {
        super(name, hp, atk, exp, type);
        this._DEF = def;
    }
// ----------------------------------------------------------------------
    Mon_Hitted(hit_damage)
    {
        const damage = hit_damage * ((100 - this._DEF)/100);
        this._HP -= damage;
        return chalk.blue(`몬스터가 ${damage} 의 피해를 입었습니다.`);
    }
// ----------------------------------------------------------------------
    get DEF() { return this._DEF; }
}

 

보스 몬스터는 일반 몬스터 클래스를 상속하는 자식 클래스로 구성하였다.

해서 일반 몬스터에서 추가적으로 생기는 수치, 기능들을 override 를 통해 재정의하였으며 특히 피격 메서드(Mon_Hitted) 가 주요하다.

플레이어와 동일하게 방어력 수치(DEF)를 가지고 있기에 동일한 피격 데미지 처리 수식을 적용해주었다.

 

몬스터 데이터 수치

더보기
[
    {
        "stage" : 1,
        "type" : "Normal",
        "name" : "부러진 나무 줄기",
        "hp" : 50,
        "atk" : 15,
        "exp" : 40
    },

    {
        "stage" : 2,
        "type" : "Boss",
        "name" : "오염된 당산나무",
        "hp" : 200,
        "atk" : 25,
        "exp" : 100,
        "def" : 25
    },

    {
        "stage" : 3,
        "type" : "Normal",
        "name" : "어미잃은 코브라",
        "hp" : 60,
        "atk" : 40,
        "exp" : 50
    },

    {
        "stage" : 4,
        "type" : "Boss",
        "name" : "흉폭한 식인 악어",
        "hp" : 300,
        "atk" : 50,
        "exp" : 120,
        "def" : 30
    },

    {
        "stage" : 5,
        "type" : "Normal",
        "name" : "풍화된 석회암",
        "hp" : 120,
        "atk" : 25,
        "exp" : 60
    },

    {
        "stage" : 6,
        "type" : "Boss",
        "name" : "분출하는 용암석",
        "hp" : 500,
        "atk" : 35,
        "exp" : 150,
        "def" : 50
    },

    {
        "stage" : 7,
        "type" : "Normal",
        "name" : "현혹된 피라미",
        "hp" : 80,
        "atk" : 50,
        "exp" : 70
    },

    {
        "stage" : 8,
        "type" : "Boss",
        "name" : "노래부는 세이렌",
        "hp" : 400,
        "atk" : 50,
        "exp" : 180,
        "def" : 40
    },

    {
        "stage" : 9,
        "type" : "Boss",
        "name" : "어린 마녀",
        "hp" : 6666,
        "atk" : 66,
        "exp" : null,
        "def" : 66
    }
]

스테이지별로 각자의 타입, 이름, 체력 등 여러 수치를 기록하였다.

이 정보들은 개발 구성이라기보단 기획적인 측면이 강해서 자세한 설명은 생략하지만, 대략적으로 말하자면 소울류의 특성을 살리기 위해 보스의 체력을 일반 몬스터보다 강하게 하면서 각자 이름에 걸맞는 특성을 위해 미세한 수치 조정이 들어갔다고 보면 될 것 같다.

 

Ch7. EndGame.js

위에서 설명한 모든 과정, 즉 전투가 끝나고 Rogue.js로 돌아오면 while 문이 끝나고 EndGame() 함수를 호출한다.

그 경우 EndGame.js 파일로 넘어와 엔딩 스크립트 기능을 실행한다.

import chalk from "chalk";
import { start } from "./Server.js";
import fs from "fs";
import readlineSync from 'readline-sync';

export async function EndGame() 
{
  const data = fs.readFileSync("EndGameMessage.json", "utf-8");
  const messages = JSON.parse(data);

  let m_Id = 1;
  while (m_Id <= messages.length) {
    console.clear();
    const messageObj = messages.find((message) => message.id === m_Id);
    console.log(chalk.white(messageObj.message));

    if (m_Id === 6) choiceExitOrMore(messages, m_Id);
    readlineSync.question(chalk.gray("\n Press Enter ..."));
    m_Id++;
  }
}

function choiceExitOrMore(messages, id) 
{
  console.log(chalk.magentaBright(`\n1. 좋아. 2. 그만 할래`));
  const choice = readlineSync.question(chalk.gray("Enter : "));
  switch (choice) {
    case "1":
      console.clear();
      messageObj = messages.find((message) => message.id === id + 2);
      console.log(chalk.white(messageObj.message));
      readlineSync.question(chalk.gray("\nStart!"));
      start();
      break;
    case "2":
      console.clear();
      messageObj = messages.find((message) => message.id === id + 1);
      console.log(chalk.white(messageObj.message));
      process.exit(0);
    default:
      console.clear();
      console.log(chalk.white("뭐라고? 제대로 못들었어."));
      choiceExitOrMore();
  }
  let messageObj;
}

 

EndGame()

비동기 함수의 동기적 표현으로 처리하여 외부에서 이 함수를 호출할 때 기능이 끝날 때까지 기다리도록 하여 프로세스의 종료를 대기할 수 있도록 하였다.

내부적으로는 spawnMonster() 함수와 동일한 과정으로 엔딩 스크립트들을 담은 EndGameMessage.json 파일을 받아오고 이를 while 문을 통해 하나씩 출력하도록 하였다.

또한 연쇄적으로 한번에 출력되는 것을 방지하기 위해 매 호출마다 readlineSync 를 통해 사용자의 입력을 받을 때까지 멈추도록 구현하여 자연스러운 대화 흐름을 연출하였다.

이후 플레이어가 게임을 종료할지, 처음부터 재시작할 지 결정되는 순간에 choiceExitOrMore 함수를 호출하여 선택지를 제공할 수 있게 구성하였다.

choiceExitOrMore(messages, id)

이 함수가 호출되는 타이밍, m_Id === 6 일 때의 스크립트는 다음과 같다.

따라서 선택지를 2개, '1. 좋아   2. 그만 할래' 로 제공하여 입력은 readlineSync 로, 후처리는 switch 문을 통해 진행하도록 했다.

1번의 경우 현재 id값에 2를 더하여 그에 맞는 스크립트를 불러올 수 있도록 하였고 처음 프로세스 시작 시 실행되는 함수인 start() 함수를 호출하여 게임을 처음부터 진행할 수 있도록 하였다.

2번의 경우 현재 id값에 1을 더하여 마찬가지로 그에 맞는 스크립트를 불러오고 자연스럽게 게임을 종료하도록 구성하였다.

이 함수에서 의외라고 생각할 수 있는 부분이 하단의 let messageObj; 인데, C/C++ 등의 다른 언어라면 저 위치에 선언되는 것이 매우 부적절한 흐름이지만 자바스크립트에서는 호이스팅 과정을 통해 선언부는 모두 제일 먼저 실행되기 때문에 버그 없이 진행이 가능하다.

 

EndGameMessage.json

더보기
[
    {
        "id":1,
        "message": "크윽! 어린 마녀는 쓰러졌다..!"
    },

    {
        "id" : 2,
        "message" : " ..... "
    },

    {
        "id" : 3,
        "message" : "아빠, 재미있었다 그치?!"
    },

    {
        "id" : 4,
        "message" : "조금만 더 하면 내가 이기는 거였는데..."
    },

    {
        "id" : 5,
        "message" : "그래도 다음에는 내가 이길거야. 두고 봐!"
    },    

    {
        "id" : 6,
        "message" : "어때? 한번 더 놀래?!"
    },   

    {
        "id" : 7,
        "message" : "... 재미 없어."
    },    

    {
        "id" : 8,
        "message" : "좋아! 이번엔 꼭 내가 이길거야!"
    }
]

각자의 순서에 맞게 id 값과 message를 입력해두었다.

EndGame 이 호출되고 나면 다음과 같이 진행된다.

 

이렇게 로그라이크 게임 '로그소울' 의 모든 프로세스를 진행한다.

 

관련글 더보기