시리즈물로 제작 중입니다. 이전 내용과 이어집니다.
https://songmin9813.tistory.com/7(6. Deleterow)
소마 최종 평가를 앞두고 이런 거 저런 거 하느라 글이 늦어졌습니다. 양해 부탁드립니다.
7. Novelits
1. 텍스트 기반 게임 기획 의도
중간 평가가 끝난 이후 장점과 더불어 꽤나 첨예한 피드백을 받았던 기억이 난다. 그중에 가장 인상에 깊었던 피드백 하나를 소개한다.
저는 사진같은 거 별로 안 좋아하는데, 읽을거리 같은 것도 제공해주면 좋을 것 같아요…
나이가 어느정도 있었던 분이라 이 부분은 이해할 수 있었다. 오히려 내 쪽에서 고려를 하지 않은 케이스라고 할까. 이런 피드백은 추후 ‘소설에 기반한 게임’을 만들기로 한 결정적인 계기가 되었다.
애초에 첫 게임을 기획할 때 상호작용이 두드러지는 보드게임들을 정말 많이 봐왔다. 그중에 대표적인 것과 그 메커니즘을 몇 가지 소개하면서 이 이야기를 시작하는 것이 좋을 것 같다.
🎮 그날 밤 누군가 죽었다(카드 이야기 금지), 화이트홀 미스터리(현재 위치 이야기 금지), 클루(정보의 노출 최소화)
위 세 가지 게임의 공통점을 파악할 수 있겠는가? 바로 ‘규칙’으로만 플레이어를 제한한다는 점이었다. 게임이라는 존재 자체가 ‘규칙’이라는 통제된 환경 속에서 플레이어간/환경 간 상호작용으로 플레이를 이어나간다는 특징을 가진다.
하지만 이 점이 동기부여가 가장 낮은 시기인 아침에도 포함이 될 수 있는 항목일까 싶었다. 규칙으로만 플레이어를 통제한다 하면, 졸린 아침 시간대에 과연 플레이어가 그 규칙을 따라 상호작용을 하려 들까? 되려 자신의 정보를 빨리 말하고, 또는 승패 결정을 빨리 보고 다시 잠에 들지 않을까? 이런 악용(어뷰징)의 가능성을 배제할 수 없었다는 것이다.
본인은 이 어뷰징의 영역이 가장 두드러지는 항목으로 ‘텍스트에 기반한 규칙’이라 정의를 내리고 최대한 배제하는 쪽으로 첫 기획을 잡았던 것으로 기억한다.
하지만 이는 텍스트 기반의 게임을 선호하는 사용자들을 배려하지 않은 결정이라는 것을 알게 되었다. 귀여운 것이 아닌 책을 읽으면서, 또는 머리를 쓰는 것을 선호하는 사용자도 있을 것이고 본인은 이를 최대한 아우를 수 있는 게임들을 만들어야 함을 이번 중간 평가를 보내면서 뼈저리게 느끼게 되었다.
이것이 내가 이미지 기반 게임을 만들다 선회하여 텍스트 기반의 게임을 몇 만들기로 한 계기이지 않았을까.
💡 게임에는 정답이 없다. 지금까지 그저 내가 재미를 느끼는 요소들 몇몇을 게임 안에 녹여냈을 뿐.
이러한 이유로 해당 게임은 첫 번째 텍스트 기반 게임이자 Maestro 기간 중 네 번째로 개발한 게임이 되었다.
해당 프로젝트의 기원은 텍스트도 텍스트이지만 내 오랜 지인 중 한 명이 ‘플랫폼 작가’로 활동하고 있던 것이 인사이트에 가장 많은 도움을 주었다.
스토리텔링 기법을 사용하되, 최소한의 개발과 인력을 이용하여 많은 콘텐츠를 뽑아낼 수 있는 소재로 ‘소설’이라는 장르를 선택하였다.
타 작가분들과 협업하면서 텍스트의 대표격이라 할 수 있는 ‘소설’을 이용하고, 이를 하루에 2~3 문장씩 읽어주는 형식으로 사용자 간의 상호작용이 이루어지면 어떨까? 본인에게는 꽤나 흥미로운 아이디어인 것 같았다.
지인 중에 항상 밤부터 아침까지(잘 때도!) 전화통화를 하는 커플에서 인사이트를 받기도 하였다.
2. 플레이 방식
상기 두 화면을 출력해내기 위해 필요한 입력 데이터는 다음과 같다.
const inputData = {
currentIndex: "current novel's index(pages)",
otherUserIndex: "other user's current index",
text: [
"novel text",
],
totalIndex: "current novel's total index(pages)"
};
각 사용자 A와 B의 화면 구성은 위의 그림과 같다. 여러 글씨체 중 가독성이 가장 좋다고 생각되는, 그리고 여러 사람들의 선택을 통해 제주도에서 자체 제작한 ‘제주 명조체’를 사용하기로 하였다.
사용자 A와 B의 역할이 나뉘어져 있는 것 같지만, 검사에 통과하게 되면 서로의 역할을 바꾸어 다시 한번 진행되기에 이 게임을 역할 비대칭형 게임이라고는 볼 수 없을 것 같다.
해당 게임의 핵심 플레이 방식은 다음과 같다.
- ‘말하기’를 담당하는 사용자는 자신에게 출력되는 글을 말하면서 상대방이 잘 적어낼 수 있도록 도와주어야 한다.
- 반대로 ‘받아쓰기’를 담당하는 사용자는 말하기 사용자의 말을 잘 듣고 최대한 따라 쓰는 역할을 수행하여 80% 이상의 유사도 판정을 받아내야 한다.
추가로 게임을 진행하면서 발생할 수 있는 경우는 다음과 같다.
- 두 사용자의 index가 같은 경우 한 사용자의 currentIndex+1의 텍스트를 호출해야 한다.
- 1의 경우, nextIndex는 두 사용자 모두 currentIndex+2된 값으로 세팅(연속 콘텐츠 보너스)
- 두 사용자의 index가 다른 경우 각 사용자의 nextIndex는 currentIndex+1로 세팅(솔로 콘텐츠 소비)
- 한 사용자가 소설을 모두 읽었을 경우 다른 사용자의 currentIndex+1에 해당하는 텍스트를 호출한다.
- 두 사용자가 소설을 모두 읽었을 경우 randomIndex, randomIndex+1로 세팅한다.
위의 경우는 한 게임에 최대한 다른 텍스트를 주어야 함에 기인하며, 인덱스 추가에 대해서는 항상 totalIndex를 비교하며 인덱스 허용 범위를 벗어나지 않았는지 확인하여야 한다.
const indexLogic=(indexA,indexB,totalIndex)=>{// A의 인덱스, B의 인덱스, 소설 총 인덱스
const randomIndex=Math.ceil(Math.random()*100)%totalIndex-1;// 마지막 페이지를 제외한 랜덤 인덱스
if(indexA === totalIndex){// A가 다 읽음
if(indexB === totalIndex) return [randomindex, randomIndex+1]; // 다 읽었음+다 읽었음
else if(indexB+1 === totalIndex) return [randomIndex, indexB]; // 다 읽었음+마지막 페이지
else return [indexB, indexB+1]; // 다 읽었음+중간 페이지
}
if(indexB === totalIndex){// B가 다 읽음
if(indexA+1 === totalIndex) return [indexA, randomIndex]; // 마지막 페이지+다 읽었음
else return [indexA, indexA+1]; // 중간 페이지+다 읽었음
}
if(indexA===indexB){// 두 인덱스가 같은 경우
if(indexA+1===totalIndex) return [indexA, randomIndex];// 둘 다 마지막 페이지
else return [indexA, indexB+1];// 둘 다 중간 페이지
}
else return [indexA, indexB];// 둘 다 인덱스가 다른 중간 페이지
};
소설 콘텐츠에 대해서는 바로 ‘이계절’ 작가를 섭외하여 가장 가볍게 읽을 수 있는 ‘캐롤 찾기’라는 소설을 이식하는 작업에 착수하였다.
추가적으로 한 게임에 대한 사용자별 문장 개수는 안드로이드 사용자의 평균 타수인 180타를 기준점으로 삼았다. 최대 게임 시간은 5분을 넘기지 않게 설정하면서, 여유롭게 말을 하여도 충분히 클리어가 가능한 250자+250자=한 게임당 500자 정도의 문장 길이로 콘텐츠 길이를 세팅하였다.
3. 핵심 알고리즘 - 레벤슈타인 거리(NLP)
검사에 대해서는 전통적인 NLP 기술 중 ‘몇 번의 수정/갱신으로 원본 스트링이 바뀌는지’에 대한 결과를 반환하는 ‘레벤슈타인 거리’ 알고리즘을 사용하기로 하였다.
(Levenshtein distance - Wikipedia)
해당 알고리즘은 두 개의 문장을 2차원에 배열로 나타내기에 일종의 Dynamic Programming(DP) 기법이라는 특징을 가진다. 해당 알고리즘의 특징상 복잡도가 N*N번으로 수행되어 문장의 길이가 길어질 경우 수행 시간이 급격히 증가될 수 있다는 단점이 있다.
하지만 애초에 최대 문장 길이를 250자로 제한(최대 62,500 연산)했기에 실시간 연산을 수행하여도 충분히 수행 가능한 영역임을 확인. 바로 해당 알고리즘을 사용하기로 하였다.
레벤슈타인 거리를 찾아내는 알고리즘은 다음과 같다.
const regExp=(str)=>{
const reg=/[♥\\{\\}\\[\\]\\/?.,;:| \\)*~`!^\\-_+<>@\\#$%&\\\\\\=\\(\\'\\"]/gi;
//define stopword
if(reg.test(str)) return str.replace(reg,"");
else return str;
};
const getDistance=(origin,test)=>{
const table=new Array(origin.length+1);
table.fill(0);
for(let i=0;i<table.length;i++){
table[i]=new Array(test.length+1);
table[i].fill(0);
}
for(let i=1;i<=origin.length;i++)table[i][0]=i;
for(let j=1;j<=test.length;j++)table[0][j]=j;
for(let i=1;i<=origin.length;i++){
for(let j=1;j<=test.length;j++){
const ins=table[i-1][j]+1;
const del=table[i][j-1]+1;
const rep=(origin.charAt(i-1)===test.charAt(j-1)?0:1)+table[i-1][j-1];
table[i][j]=Math.min(ins,del,rep);
}
}
return 100-Math.floor(table[origin.length][test.length]/origin.length*100);
//return percentage
};
해당 알고리즘을 사용하여 어느 정도 비슷하게 흉내만 내어도 80% 이상의 유사도가 나오는 것을 확인. 정확한 검사 기준은 비공개로 두되, 사용자로 하여금 빡빡한 검사를 유도하지 않게 해당 게임을 기획하였다.
실제로 아래의 두 문장을 넣고 테스트를 돌렸을 때 70%의 유사도가 나오는 것을 알 수 있었다.
'남자가 고백해야지!'라는 것도 있었다.지금은 깔끔히 없어진 생각이지만 스무 살에는 그렇게 생각했다.
'남자가 고벡해야지'라는것도있었다.지금은말끔히 없어진셍각이지만 슴살엔 그렇게 생각했다!
딱 생각보다 빡빡하지 않으면서, 집중하면서 들었다고 표시는 나야 통과되는 수준인 것 같다.
'개발 > 소프트웨어 마에스트로' 카테고리의 다른 글
SWM에서 게임 개발로 살아남기(8. Manyfest) (0) | 2022.12.13 |
---|---|
SWM에서 게임 개발로 살아남기(6. Deleterow) (0) | 2022.11.22 |
SWM에서 게임 개발로 살아남기(5. Twigitizer) (0) | 2022.11.20 |
SWM에서 게임 개발로 살아남기(4. Picoke) (0) | 2022.11.18 |
SWM에서 게임 개발로 살아남기(3. 게임 데이터 수집) (0) | 2022.11.16 |