개발/소프트웨어 마에스트로

SWM에서 게임 개발로 살아남기(6. Deleterow)

잠수돌침대 2022. 11. 22. 16:46

시리즈물로 제작 중입니다. 이전 내용과 이어집니다.

https://songmin9813.tistory.com/6(5. Twigitizer)


6. Deleterow

이미지 기반 게임의 두 번째 게임이자 Maestro 기간 중 세 번째로 기획하게 된 게임이다. 해당 게임을 만들게 된 시기도 중간 평가도 끝나 있고, Picoke에 대한 피드백도 어느 정도 받은 상태였기에 새로운 콘셉트를 만드는 것에 집중을 했던 시기였다.

 

기획 평가 때 이미지(게임 데이터)를 재활용하여 다른 게임 제작이 용이할 수 있다는 언급을 하여서 이를 실제 다른 게임을 제작하면서 검증을 해보고 싶었다. 실제로 Picoke 코드를 일부 참고하거나 약간만 수정하여 개발 자체의 기간은 그리 길지 않았던 것으로 기억한다.

 

💡 하나의 게임 데이터를 여러 번 활용할 수 있다는 데이터의 확장성을 보여주고 싶었다…!

 

상기 Picoke 게임의 제약사항에 개인적인 욕심과 더불어 추가적으로 구현하고 싶은 항목은 다음과 같았다.

 

  1. 클라이언트에서 자체 검사하는 것이 아닌, 실제로 실시간 소통(메시지) 방식으로 검사를 진행해보자
  2. 정보의 비대칭을 가지는 Picoke 게임. 이제는 역할의 비대칭을 띠는 게임도 하나 만들어보자

 

1. FE 와의 실시간 소통 방법 명시

 

이런 저런 검사와 더불어 클라이언트끼리 소통을 하기 위해서는 혼자서 개발을 하는 것이 아닌 FE와의 원활한 협업이 필수적으로 요구되었다. Picoke 게임에서 사용되는 postMessage와 listener 부분을 고도화시킬 필요성을 느꼈고 노션을 이용하여 주기적으로 소통하며 자신이 어떤 방식으로 소통을 진행할 것인지, 이게 현실적으로 가능한 요청인 것인지에 대한 교차 검증 작업 등을 진행했다.

 

아래의 목록은 노션으로 진행되었던 메시지 구조화 작업을 코드로 표현한 작업이다.

 


 

유저별로 다른 화면을 뿌리게 하기 위해서는 코드 안에서 현재 유저를 구분할 수 있는 유저 정보가 필요함(가령 A, B와 같은 형식)

 

1. 현재 자신의 유저 정보 반환(삭제 가능성 있음)

//input
const result = JSON.stringify({
	type: "GET_MY_USERTYPE",
	message: ""
});
window.ReactNativeWebView.postMessage(result);

//output
if(type === "MY_USERTYPE"){
	currentUser = message.user; // return current usertype
}

 

2. 현재 상대방의 유저 정보 반환(아직 2인을 가정함. 여럿은 모르겠네 이거)

  •  
//input
const result = JSON.stringify({
	type: "GET_MATE_USERTYPE",
	message: ""
});
window.ReactNativeWebView.postMessage(result);

//output
if(type === "MATE_USERTYPE"){
	mateUser = message.user; // return mate usertype
}

 

상기 구조화된 내용을 바탕으로 FE는 실시간 메시지(Real Time Message : RTM)가 적용되도록 구현을 하였으며, 이는 이후에 만들어지는 Deleterow 코드에도 녹아들어 있다.

 


2. 역할의 비대칭 게임 = 코드도 두 개

 

기존 Picoke 게임의 사용자 A 화면과 B의 화면 구성은 이미지만 다를 뿐, 그 본질은 구조적으로 동일하다. 본인은 이를 ‘정보의 비대칭’이라는 표현을 사용하였는데, 비대칭이라는 정보를 정말 다양한 시선에서 바라보고 이를 구현해보고 싶은 욕심이 문득 들었다.

 

이에 사용자 A와 B 별로 다른 미션을 부여하여 서로가 보는 화면은 다르되, 협력을 통하여서 클리어해야 하는 방식은 그대로 유지하는 ‘역할의 비대칭’ 방식을 이용한 게임을 제작하기로 하였다.

 

결국 최초로 필요한 것은 이 사람이 사용자 A인지 B인지에 대한 정보였고, 이는 추후에 모든 게임이 시작될 때 필요한 필수적인 정보인 것 같아 FE에게 게임 시작 시 클라이언트마다 명시를 부탁하였다.

 

if (type === "GAME_START") {
    const { currentUser, images } = message; //init currentUser data
    if (currentUser === "A") otherUser = "B"; 
		else otherUser = "A";
    inputData.images = images;
    currentUser === "A" ? userAGrid() : userBGrid();
  }

 

위의 코드를 통하여 Game은 FE에게 이 사람이 A/B 유저 여부를 부여받게 되고, 이를 통하여 Game은 역할별로 다른 미션을 줄 수 있는 형태로 확장되었다.

 

사용자 A와 B의 화면

 

게임의 방식은 사용자 A의 설명을 최대한 듣고, 사용자 A에게 출력되는 이미지가 아닌 것 같은 이미지를 찾아 연속으로 5개를 삭제시켜야 하는 게임으로 제작하였다.

 

게임의 이름은 가제 Pic-Remove 프로젝트. 이후 연속해서 삭제시킨다는 뜻의 ‘Delete in a row’의 말을 축약하여 Deleterow라는 이름으로 확정 지었다.

 

사용자 B의 화면에서도 보다시피 기존에 사용되었던 Picoke 코드의 HTML, CSS 정보를 최대한 재활용하는 쪽으로 사용하며 개발 시간을 단축하였다. 추가적으로 개발되는 사용자 A에 대한 HTML 정보를 작성하고, 이를 JS를 이용해 분기하여 동적으로 body 안에 넣는 작업을 포함시켰다.

 

//part of userA.js
const userAGrid=()=>{
    gameDocument.innerHTML=`
    <div class="grid" stlye="width:100vw;height:100vh;display: grid; grid-template-rows: 20% 70% 7%; gap:1%;text-align: center;animation: fadein 1s;">
    <div class="userA-info">
        <p>이 그림을 설명하세요</p> 
        <p>5콤보를 달성해야 합니다!</p>
    </div>
    <div class="userA-game">
        <div class="A-pic">
            <img class="pic-one hidden"></img>
            <img class="pic-two hidden"></img>
            <img class="pic-three hidden"></img>
            <img class="pic-four hidden"></img>
            <img class="pic-five hidden"></img>
            <img class="pic-six hidden"></img>
        </div>
        <div class="combo">
        <p class="combo0">0 combo!</p>
        <p class="combo1">1 combo!</p>
        <p class="combo2">2 combos!</p>
        <p class="combo3">3 combos!</p>
        <p class="combo4">4 combos!</p>
        </div>
    </div>
    <div class="answer">
        <div class="warning-watch hidden">
            <div class="lock-image"></div>
            <div class="lock-remain"><p>01:00</p></div>
        </div>
        <div class="stop-watch">
            <div class="clock-image"></div>
            <div class="timer"><p>00:00</p></div>
        </div>
        <div class="check hidden"><p>삭제</p></div>
    </div>
</div>
    `;
}

//part of userB.js
const userBGrid=()=>{
    gameDocument.innerHTML=`
    <div class="grid" style="width:100vw; height:100vh; display:grid; grid-template-rows:20% 70% 7%; gap:1%; text-align:center; animation:fadein 1s;">
    <div class="userB-info">
        <p>설명과 다른 그림을 하나씩 없애세요</p> 
        <p>5콤보를 달성해야 합니다!</p>
    </div>
    <div class="userB-game">
        <div class="B-pic">
            <img class="pic-one"></img>
            <img class="pic-two"></img>
            <img class="pic-three"></img>
            <img class="pic-four"></img>
            <img class="pic-five"></img>
            <img class="pic-six"></img>
        </div>
    <div class="combo">
    <p class="combo0">0 combo!</p>
    <p class="combo1">1 combo!</p>
    <p class="combo2">2 combos!</p>
    <p class="combo3">3 combos!</p>
    <p class="combo4">4 combos!</p>
    </div>
    </div>
    <div class="answer">
        <div class="warning-watch hidden">
            <div class="lock-image"></div>
            <div class="lock-remain"><p>01:00</p></div>
        </div>
        <div class="check"><p>삭제</p></div>
        <div class="stop-watch">
        <div class="clock-image"></div>
        <div class="timer"><p>00:00</p></div>
    </div>
</div>
    `;
}

 

이렇듯 userA와 userB의 역할별로 코드를 분리하여 작업하려 했고, 이를 제외한 함수들은 game.js에서 모두 사전 정의해놓아 작업하는 방식을 선택하였다.

 

💡 지금 다시 생각해보면 추상화를 이용한다면 조금 더 깔끔하고 완벽하게 분리된 코드를 만들 수 있을 것 같았다. 그때는 왜 이런 생각을 하지 못했을까

 

A가 하는 일은 크게 다음과 같다.

  1. 현재 사용자 B에게 보이는 이미지들을 기준으로 랜덤으로 현재의 정답을 세팅한다.
  2. 사용자 B에게 특정 이미지를 수신받아 해당 이미지가 현재의 정답이 아닌지 체크한다.
  3. 정답이라면 콤보는 초기화, 정답이 아니라면 콤보가 누적된다.
  4. 5 콤보를 달성했다면 사용자 B에게 게임이 끝났다 통보하고 자신은 게임을 종료한다.

 

B가 하는 일은 크게 다음과 같다.

  1. 사용자 A의 설명을 듣고 해당 설명과 벗어나는 그림을 선택하여 삭제를 시도한다.
  2. 사용자 B의 허가가 떨어진다면 콤보는 누적되고 선택된 그림은 삭제된다.
  3. 만약 그렇지 않다면 삭제된 모든 그림이 복구되고 다시 5콤보를 달성해야 한다.
  4. 사용자 A에게 게임 종료 통보를 받았다면 게임을 종료한다.

 

추가적으로 고려한 사항들은 다음과 같다.

  1. 연속하여 5번을 맞춰야 하기 때문에 무작위 삭제로 인한 클리어가 거의 불가능할 것이라는 판단을 하였다. 이에 연속 득점에 실패해도 Picoke과 다르게 화면 잠금 기능은 따로 추가하지 않았다.
  2. 어느 정도 찍을 수는 있어도 찍는 데에 운의 요소를 강하게 받는다는 이유+게임을 진행하면서 그림 자체가 삭제되어 게임이 점진적으로 쉬워진다는 점을 들어 게임의 난이도는 Picoke보다 비슷할 것으로 예상했다.
  3. 사용자 A 입장에서 세팅되어야 하는 정답은 실시간으로 갱신되어야 했다. 이미 정적으로 만들어놓으면 사용자 B가 미리 세팅해놓은 의 정답을 삭제할 수도 있어 로직 상의 오류가 발생할 수 있기 때문이었다.