본문 바로가기
Project

[게임 개발] 게임 중 발생하는 이벤트 추가하기

by 배애앰이 좋아 2024. 12. 27.
반응형

 

안녕하세요!

 

오늘은 이벤트 제작하는 방법에 대해 작성 해볼까 합니다. 해당 글은 기존 프로젝트에 이어서 하는 것이기 때문에 내용을 모르시면 이해하는데 어려우실 수 있어 기존 글 링크 달아드립니다.

 

내용 1 : 사각 타일 맵 기반 게임 만들기 [기획에 대한 내용이라 추천] : https://88-it.tistory.com/438

내용 2 : 자동 맵 만들기 [안 읽어도 괜찮을 부분] : https://88-it.tistory.com/439

내용 3 : 코드 리팩토링 + 데이터 저장 / 로드 [안 읽어도 괜찮을 부분] : https://88-it.tistory.com/440

 

이벤트를 추가하게 된 계기는 기존 게임에서는 단순히 움직이면서 적을 피하는 것이 전부라 완성하고 직접 테스트 해보니 재밌을 거라는 제 예상과 다르게 재미가 없더라고요…. 그래서 재미를 높이기 위해 이벤트를 중간 중간 넣어주면 좀 괜찮지 않을까 싶어서 이벤트를 구현하게 되었습니다.

 

기획한 이벤트 방식은 n 초마다 이벤트 종류가 여러 개 있고 그 중에 한 개를 무작위로 실행시켜 주는 방식입니다. 현재 기획한 이벤트 종류는 총 8개이며

 

  1. 특정 텀동안 플레이어 속도 증가
  2. 특정 텀동안 플레이어 속도 감소
  3. 플레이어 잠시 못 움직이기
  4. 특정 텀동안 적 속도 증가
  5. 특정 텀동안 적 속도 감소
  6. 적 잠시 안 움직이기
  7. 무작위로 이동시켜주는 텔레포트 생성
  8. 특별한 발판 생성으로

 

플레이어에게 유리한 것, 유리하지 않은 것 반반 섞여있으며 8번째는 이벤트에 이벤트를 더해주는 식으로 예를 들어 특별한 발판을 3번 밟으면 확률이 낮은 아이템이 강제로 나오거나 등 할 예정이지만 글을 적는 현재로서는 아직 완성하지 못했습니다.

 

위 기반으로 구현한 EventManager 코드입니다.

 

class Event {
    constructor() {
        if (new.target === Event) {
            throw new TypeError("Cannot construct Event instances directly");
        }
    }
    trigger() {
        throw new Error("Method 'trigger()' must be implemented.");
    }
    update(dt){
        throw new Error("Method 'update()' must be implemented.");
    }
    reset(){
        throw new Error("Method 'reset()' must be implemented.");
    }
}

class BlockEvent1 extends Event {
    constructor(){
        super();  
        this.blockHole1 = WORLD.getObject('blockHole1');
        this.blockHole2 = WORLD.getObject('blockHole2');
        this.blockHole1.visible = false;
        this.blockHole2.visible = false;
        this.player = null;
        this.blocks = null;
        this.isTrigger = false;
        this.resetTimeout = null;
    }
    trigger(blocks, player) {
        REDBRICK.Signal.send("CreateEvent", { eventName : "BlackHole", isActive : true});
        this.isTrigger = false;
        this.player = player;
        this.blocks = blocks;
        let randomPos = Math.floor(Math.random() * this.blocks.length);
        // 플레이어랑 붙어있지 않도록
        while (this.blocks[randomPos] && this.blocks[randomPos].hasWall === true){
            randomPos = Math.floor(Math.random() * this.blocks.length);
        }
        this.blockHole1.position.set(this.blocks[randomPos].position.x, this.blocks[randomPos].position.y + 0.5, this.blocks[randomPos].position.z);
        this.blockHole1.visible = true;
    }
    update(dt){
        let distanceToPlayer = this.blockHole1.position.distanceTo(this.player.position);
        if(distanceToPlayer < 3 && this.isTrigger === false){
            this.isTrigger = true;
            let randomPos = Math.floor(Math.random() * this.blocks.length);
            // 플레이어랑 붙어있지 않도록
            while (this.blocks[randomPos] && this.blocks[randomPos].hasWall === true){
                randomPos = Math.floor(Math.random() * this.blocks.length);
            }
            this.blockHole2.visible = true;
            this.blockHole2.position.set(this.blocks[randomPos].position.x, this.blocks[randomPos].position.y + 0.5, this.blocks[randomPos].position.z);
            REDBRICK.Signal.send("CreateEvent", {
                eventName : "BlackHole",
                isActive : false,
                moveblock: randomPos
            });
        }  
    }
    reset(){
        if (this.resetTimeout) {
            clearTimeout(this.resetTimeout);  // 기존 타이머가 존재하면 취소
        }
        this.resetTimeout = setTimeout(() => {
            this.blockHole1.visible = false;
            this.blockHole2.visible = false;
        }, 1000);
    }
}

class BlockEvent2 extends Event {
    constructor(){
        super();  
        this.lineImg = WORLD.getObject('lineImg');
        this.lineImg.visible = false;
        this.player = null;
        this.blocks = null;
        this.isTrigger = false;
    }
    trigger(blocks, player) {
        REDBRICK.Signal.send("CreateEvent", { eventName : "SpecialBlock", isActive : true});
        this.isTrigger = false;
        this.player = player;
        this.blocks = blocks;
        let randomPos = Math.floor(Math.random() * this.blocks.length);
        // 플레이어랑 붙어있지 않도록
        while (this.blocks[randomPos] && this.blocks[randomPos].hasWall === true){
            randomPos = Math.floor(Math.random() * this.blocks.length);
        }
        this.lineImg.position.set(this.blocks[randomPos].position.x, this.blocks[randomPos].position.y + 0.5, this.blocks[randomPos].position.z);
        this.lineImg.visible = true;
    }
    update(dt){
        let distanceToPlayer = this.lineImg.position.distanceTo(this.player.position);
        if(distanceToPlayer < 3 && this.isTrigger === false){
            this.isTrigger = true;
            REDBRICK.Signal.send("CreateEvent", {
                eventName : "SpecialBlock",
                isActive : false
            });
        }  
    }
    reset(){
        this.lineImg.visible = false;
    }
}

class EnemyEvent1 extends Event {
    trigger(blocks, player) {
        REDBRICK.Signal.send("CreateEvent", {eventName : "StopEnemy", isActive : true});
        this.isStopEnemy = true;
        this.currentTime = 0;
        this.lockTime = 2;
    }
    update(dt){
        if(this.isStopEnemy){
            this.currentTime += dt;
            if(this.currentTime > this.lockTime){
                this.currentTime = 0;
                REDBRICK.Signal.send("CreateEvent", {eventName : "StopEnemy", isActive : false});
            }   
        }
    }
    reset(){
        this.isStopEnemy = false;
        this.currentTime = 0;
    }
}

class EnemyEvent2 extends Event {
    trigger(blocks, player) {
        REDBRICK.Signal.send("CreateEvent", {eventName : "SpeedDownEnemy", isActive : true});
        this.isSpeedDownEnemy = true;
        this.currentTime = 0;
        this.lockTime = 5;
    }
    update(dt){
        if(this.isSpeedDownEnemy){
            this.currentTime += dt;
            if(this.currentTime > this.lockTime){
                this.currentTime = 0;
                REDBRICK.Signal.send("CreateEvent", {eventName : "SpeedDownEnemy", isActive : false});
            }   
        }
    }
    reset(){
        this.isSpeedDownEnemy = false;
        this.currentTime = 0;
    }
}

class EnemyEvent3 extends Event {
    trigger(blocks, player) {
        REDBRICK.Signal.send("CreateEvent", {eventName : "SpeedUpEnemy", isActive : true});
        this.isSpeedUpEnemy = true;
        this.currentTime = 0;
        this.lockTime = 5;
    }
    update(dt){
        if(this.isSpeedUpEnemy){
            this.currentTime += dt;
            if(this.currentTime > this.lockTime){
                this.currentTime = 0;
                REDBRICK.Signal.send("CreateEvent", {eventName : "SpeedUpEnemy", isActive : false});
            }   
        }
    }
    reset(){
        this.isSpeedUpEnemy = false;
        this.currentTime = 0;
    }
}

class PlayerEvent1 extends Event {
    trigger(blocks, player) {
        REDBRICK.Signal.send("CreateEvent", {eventName : "StopPlayer", isActive : true});
        this.isStopPlayer = true;
        this.currentTime = 0;
        this.lockTime = 3;
    }
    update(dt){
        if(this.isStopPlayer){
            this.currentTime += dt;
            if(this.currentTime > this.lockTime){
                this.currentTime = 0;
                REDBRICK.Signal.send("CreateEvent", {eventName : "StopPlayer", isActive : false});
            }   
        }
    }
    reset(){
        this.isStopPlayer = false;
        this.currentTime = 0;
    }
}

class PlayerEvent2 extends Event {
    trigger(blocks, player) {
        REDBRICK.Signal.send("CreateEvent", {eventName : "SpeedDownPlayer", isActive : true});
        this.isSpeedDownPlayer = true;
        this.currentTime = 0;
        this.lockTime = 5;
    }
    update(dt){
        if(this.isSpeedDownPlayer){
            this.currentTime += dt;
            if(this.currentTime > this.lockTime){
                this.currentTime = 0;
                REDBRICK.Signal.send("CreateEvent", {eventName : "SpeedDownPlayer", isActive : false});
            }   
        }
    }
    reset(){
        this.isSpeedDownPlayer = false;
        this.currentTime = 0;
    }
}

class PlayerEvent3 extends Event {
    trigger(blocks, player) {
        REDBRICK.Signal.send("CreateEvent", {eventName : "SpeedUpPlayer", isActive : true});
        this.isSpeedUpPlayer = true;
        this.currentTime = 0;
        this.lockTime = 5;
    }
    update(dt){
        if(this.isSpeedUpPlayer){
            this.currentTime += dt;
            if(this.currentTime > this.lockTime){
                this.currentTime = 0;
                REDBRICK.Signal.send("CreateEvent", {eventName : "SpeedUpPlayer", isActive : false});
            }   
        }
    }
    reset(){
        this.isSpeedUpPlayer = false;
        this.currentTime = 0;
    }
}

class EventFactory {
    static createEvent(type) {
        switch(type) {
            case 'block1':
                return new BlockEvent1();
            case 'block2':
                return new BlockEvent2();
            case 'player1':
                return new PlayerEvent1();
            case 'player2':
                return new PlayerEvent2();
            case 'player3':
                return new PlayerEvent3();
            case 'enemy1':
                return new EnemyEvent1();
            case 'enemy2':
                return new EnemyEvent2();
            case 'enemy3':
                return new EnemyEvent3();
            default:
                throw new Error("Unknown event type");
        }
    }
}

class EventManager{
    constructor(blocks, player, spawnTime){
        this.blocks = blocks;
        this.player = player;
        this.spawnTime = spawnTime;
        
        this.onEvent = false;
        this.currentTime = 0;
        this.isGameStart = false;
        this.eventTypes = ['block1', 'block2','player1', 'player2','player3','enemy1','enemy2','enemy3']; // 이벤트 종류 리스트
        this.event = null;
        this.currentEventType = null;
        
        this.init();
    }
    init(){
        this.currentTime = 0;
        this.isGameStart = false;
        this.onEvent = false;
        this.event = null;
    }
    Update(dt){
        if(this.isGameStart){
            this.currentTime += dt;
            if(this.currentTime > this.spawnTime){
                if(this.onEvent){
                    // if(this.event) this.event.reset();
                    // this.event = null;
                }else{
                    this.createEvent();
                }
                this.currentTime = 0;
            }   
            if (this.onEvent && this.event) {
                this.event.update(dt);
            }
        }
    }
    createEvent() {
        this.onEvent = true;
        
        let randomIndex;
        do {
            randomIndex = Math.floor(Math.random() * this.eventTypes.length);
        } while (this.eventTypes[randomIndex] === this.currentEventType);  // 이전과 동일한 이벤트가 연속해서 나오지 않도록
        this.currentEventType = this.eventTypes[randomIndex];
        //this.currentEventType = this.eventTypes[1];
        
        this.event = EventFactory.createEvent(this.currentEventType);
        this.event.trigger(this.blocks, this.player); // 이벤트 발생
    }
    resetEvent(){
        this.onEvent = false;
        this.currentTime = 0;
        this.event.reset();
    }
    Reset() {
        if(this.event) this.event.reset();
        
        this.init();
        
    }
}

GLOBAL.EventManager = EventManager;

 

앞으로도 계속 이벤트를 더 추가할 수 있어서 수정과 확장이 편리하게 하기 위해 Event 추상 클래스를 정의하고, 이를 상속한 BlockEvent1, EnemyEvent1, PlayerEvent1와 같은 구체적인 이벤트 클래스를 구현함으로써 이벤트 처리 로직을 유연하게 관리할 수 있도록 하였습니다. 새로운 이벤트 타입을 추가하려면 Event 클래스를 수정하지 않고, EventFactory에 새로운 이벤트 클래스를 추가하는 것만으로 확장 가능해 유지보수성을 높일 수 있도록 하였습니다.

 

이벤트를 받은 GameManager 코드 일부 :

 

// ...

function Start() {
		// ...
    REDBRICK.Signal.addListener("CreateEvent", (params)=>{
        const moveblock = params.moveblock !== undefined ? params.moveblock : 1;
        EventManage(params.eventName, params.isActive, moveblock);
    });
}

function EventManage(eventName, isActive, moveblock){
    currentEventName = eventName;
    console.log(eventName + " " + isActive);
    switch(eventName){
        case "BlackHole":
            if(isActive){
                GameGUI.ShowRandomBuff(true, "텔레포트 생성");
            }else{
                if(haveEvent) return;
                haveEvent = true;
                currentMoveblock = moveblock;
            }
            break;
        case "SpecialBlock":
            GameGUI.ShowRandomBuff(isActive, "특별한 블록 등장");
            if(isActive){
            }else{
                EventManager.resetEvent();
            }
            break;
        case "StopEnemy":
            if(isActive){
                isEnemyMove = false;
                GameGUI.ShowRandomBuff(true, "몬스터 이동 금지");
            }else{
                isEnemyMove = true;
                EventManager.resetEvent();
                GameGUI.ShowRandomBuff(false, "");
            }
            break;
        case "SpeedUpEnemy":
            GameGUI.ShowRandomBuff(isActive, "몬스터 속도 증가");
            if(isActive){
                EnemyManager.ChangeEnemySpeed(-0.5);
            }else{
                EnemyManager.ChangeEnemySpeed(0.5);
                EventManager.resetEvent();
            }
            break;
        case "SpeedDownEnemy":
            GameGUI.ShowRandomBuff(isActive, "몬스터 속도 감소");
            if(isActive){
                EnemyManager.ChangeEnemySpeed(0.5);
            }else{
                EnemyManager.ChangeEnemySpeed(-0.5);
                EventManager.resetEvent();
            }
            break;
        case "StopPlayer":
            GameGUI.ShowRandomBuff(isActive, "플레이어 이동 금지");
            if(isActive){
                haveEvent = true;
                isPlayerStopMove = true;
                keyImgs.forEach((img) => {
                    img.visible = false;
                });
            }else{
                hasPlayerStopEvent(false);
            }
            break;
        case "SpeedUpPlayer":
            GameGUI.ShowRandomBuff(isActive, "플레이어 속도 증가");
            if(isActive){
                playerSpeed += 1;
            }else{
                playerSpeed -= 1;
                EventManager.resetEvent();
            }
            break;
        case "SpeedDownPlayer":
            GameGUI.ShowRandomBuff(isActive, "플레이어 속도 감소");
            if(isActive){
                playerSpeed -= 1;
            }else{
                playerSpeed += 1;
                EventManager.resetEvent();
            }
            break;
        default:
            break;
    }
}

// ...

 

이벤트 발생 시 알람을 보내는 옵저버 패턴을 사용하며, 이 알람을 받은 GameManager는 각 이벤트에 맞는 처리를 수행합니다. 각 이벤트별로 알람을 받는 과정이 복잡해지기 때문에, 매개변수로 이벤트 이름과 활성화 여부를 전달하여 처리를 간소화했습니다.

 

이벤트를 발생시키면서 주의한 점은 블록을 이동 중에 이벤트가 발생될 수 있어서 특히 플레이어 움직임과 관련된 이벤트들은 이미 움직였다면 다음 지점에 도착하고 나서 이벤트를 발생 시키도록 해주었습니다.

 

현재 위의 코드에 따른 이벤트 일부 장면 :

 

어떤 이벤트가 발생 되었는지 알아볼 수 있게 왼쪽 중단에 랜덤 효과라는 UI 만들어주었습니다.

 

 

플레이어 이동 금지가 될 때는 발판이 사라지고 키를 눌려도 동작하지 않게 하였습니다.

 

 

텔레포트 생성 효과 경우, 그림과 검은 색 홀을 랜덤으로 생성해서 플레이어가 해당 지점에 도착하면 빨간 홀 또한 랜덤으로 생성해서 플레이어가 빨간 홀 쪽으로 이동하게 됩니다.

 

 

사진으로 확인하기 어려워 관련 영상 첨부드립니다.

현재까지 완성한 결과로 자동 맵, 아이템, 중간 효과 등 다 적용되어 있는 영상입니다,

 

 

이벤트 추가 이후 확실히 전보다는 더 재밌어졌지만 그래도 계속 플레이하기에는 부족하다는 생각이 드는 것 같습니다. (게임 자체 문제인가…ㅠㅠㅠ 장르를 바꿔야하나 이런 저런 생각이 많이 드네요) 어떻게 해야 플레이어가 같은 방식 게임을 여러번 하면서도 쉽게 안 질릴지 좀 더 기획해서 구현해보겠습니다.

 

여기까지 글 읽어주셔서 감사합니다.

 

 

반응형

댓글