안녕하세요!
오늘은 이벤트 제작하는 방법에 대해 작성 해볼까 합니다. 해당 글은 기존 프로젝트에 이어서 하는 것이기 때문에 내용을 모르시면 이해하는데 어려우실 수 있어 기존 글 링크 달아드립니다.
내용 1 : 사각 타일 맵 기반 게임 만들기 [기획에 대한 내용이라 추천] : https://88-it.tistory.com/438
내용 2 : 자동 맵 만들기 [안 읽어도 괜찮을 부분] : https://88-it.tistory.com/439
내용 3 : 코드 리팩토링 + 데이터 저장 / 로드 [안 읽어도 괜찮을 부분] : https://88-it.tistory.com/440
이벤트를 추가하게 된 계기는 기존 게임에서는 단순히 움직이면서 적을 피하는 것이 전부라 완성하고 직접 테스트 해보니 재밌을 거라는 제 예상과 다르게 재미가 없더라고요…. 그래서 재미를 높이기 위해 이벤트를 중간 중간 넣어주면 좀 괜찮지 않을까 싶어서 이벤트를 구현하게 되었습니다.
기획한 이벤트 방식은 n 초마다 이벤트 종류가 여러 개 있고 그 중에 한 개를 무작위로 실행시켜 주는 방식입니다. 현재 기획한 이벤트 종류는 총 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 만들어주었습니다.
플레이어 이동 금지가 될 때는 발판이 사라지고 키를 눌려도 동작하지 않게 하였습니다.
텔레포트 생성 효과 경우, 그림과 검은 색 홀을 랜덤으로 생성해서 플레이어가 해당 지점에 도착하면 빨간 홀 또한 랜덤으로 생성해서 플레이어가 빨간 홀 쪽으로 이동하게 됩니다.
사진으로 확인하기 어려워 관련 영상 첨부드립니다.
현재까지 완성한 결과로 자동 맵, 아이템, 중간 효과 등 다 적용되어 있는 영상입니다,
이벤트 추가 이후 확실히 전보다는 더 재밌어졌지만 그래도 계속 플레이하기에는 부족하다는 생각이 드는 것 같습니다. (게임 자체 문제인가…ㅠㅠㅠ 장르를 바꿔야하나 이런 저런 생각이 많이 드네요) 어떻게 해야 플레이어가 같은 방식 게임을 여러번 하면서도 쉽게 안 질릴지 좀 더 기획해서 구현해보겠습니다.
여기까지 글 읽어주셔서 감사합니다.
'Project' 카테고리의 다른 글
[게임 개발] 코드 리팩토링 + 데이터 저장 / 로드 (0) | 2024.12.15 |
---|---|
[게임 개발] 자동 타일 맵 만들기 (0) | 2024.12.08 |
[게임 개발] 사각 타일 맵 기반 게임 기초 만들기 (0) | 2024.12.01 |
[해커톤] Junction Asia 2024 참여 후기 (0) | 2024.08.19 |
[공모전/게임잼] 대한민국 No.1 서버 게임 개발 공모전 / 제 2회 포톤 게임잼 - Photon Game Jam 예선 참여 후기 및 Photon 이용 후기 (0) | 2024.08.06 |
댓글