SlideShare a Scribd company logo
졸업프로젝트 개발 최종 발표
: Unity 엔진을 이용한
고양이 3D RPG 게임 제작
정보미디어학과 16038020 김희진
프로젝트 개요
-길고양이로 태어나 혹독한 길바닥 생활에서 살아남는 고양이 rpg게임
-게임 목표: 서열 1위와 싸워서 이긴다.
-게임 진행: 스텟을 높여 자신 보다 높은 서열의 고양이들을 차례차례 쓰러트린다.
-사용 프로그램: 유니티(C#)
개념도
3D 파일 관리
지형 배치
Npc, 아이템 배치
애니메이션 설정
스크립트 관리
스크립트 작성
게임오브젝트 관리
행동 제어
스텟 제어
상호작용 제어
마우스/게임
패드 입력
플레이어
다른 고양이와
상호작용
• 서열 싸움
> 이기면 서열 상승
• 대화
> 친구가 됨 >서열 싸움 서포트
엔딩
• 서열1위
• …
기타 사물과
상호작용
• 스텟 증가
• 스텟 감소
게임의 전반적인 흐름도
프로젝트 시나리오
@캐릭터 설정 : 랜덤하게 스텟이 정해진다. (1~5 사이, 미모만 1~10사이,
모든 스텟의 최대치는 10)
*몸집 - 힘 세짐(몸싸움에서 이길 수 있음)
*목소리 - 자싞감 높아짐(기싸움에서 이길 수 있음)
*사교성 - 친구 잘 사귈 수 있음(친구랑 다니면 싸울 때 서포트해줌)
*미모 - 인기가 많아진다(인간들이 공물 바쳐줌)
캐릭터는 배부름 게이지가 있다. (최대 10. 5분에 1씩 깎임)
배부름을 채우기 위해서는 캣맘이 주는 밥을 먹거나 음식물 쓰레기를 뒤져야
한다. 음식물 쓰레기를 찾거나 캣맘이 어디에 밥을 주는지 찾아야 함
배부름 10일 때는 힘+1 버프가 지속된다.
캐릭터는 UI에서 자싞의 서열 순위를 알 수 있다.
서열 1위 고양이는 몸집10목소리10사교성1미모1인 폭군 고양이다. 서열
1위 고양이와 친구가 되려면 사교성이 10이어야 한다.
프로젝트 시나리오
캐릭터는 다른 고양이와 상호작용 할 수 있다
1. 대화
- 대화를 통해서 다른 고양이와의 호감도를 쌓을 수 있다. 사교성이 높은 고
양이는 친구 고양이를 잘 사귈 수 있다. 친구가 된 고양이는 함께 다닐 수 있
다. 친구 고양이는 서열 싸움시 서포트 해준다.
2. 서열 싸움
- 서열 싸움은 2단계로 이루어진다.
1단계(기싸움) : 기싸움은 목소리 차이가 2 이상이면 싸움을 걸 수 없다.
2단계(몸싸움) : 단순히 힘이 쎈 고양이가 이긴다. 단, 친구 고양이가 서포트
할 경우 힘 버프 (친구 고양이의 힘에 따라서 버프도 세짐)
- 서열 싸움에서 이기면 서열(레벨)이 1 올라감 + 몸집 1 커짐
@이벤트
- 지나가는 인간에게 다가가면 공물을 받을 수도 있다. 공물 종류에 따라 몸
집 or 목소리를 높여주거나 배부름 게이지를 채워준다. (크레이지아케이드
의 비행기 처럼)
- 고양이 친구가 2마리 늘어날 때마다 사교성 +1
프로젝트 시나리오
@엔딩 :
1. 서열 1위 고양이와 서열 싸움에서 이기면 보통 엔딩
2. 서열 1위 고양이와 친구가 되면 평화 엔딩
3. 처음 스텟 중 미모가 만렙이면 냥줍 엔딩
4. 배부름 게이지가 0이 되면 배드 엔딩...
5. 친구 없이 모든 고양이를 이기면 파괴왕 고양이! 히든 엔딩
생김새
이름 1호 2호 3호 4호 5호
서열 Lv 1 Lv 2 Lv 3 Lv 4 Lv 5
성격 순함 중2병 퉁명스러움 내성적 폭군
힘 3 5 7 8 10
카리스마 1 4 8 5 10
사교성 10 7 4 2 1
npc 개별컨셉
유스케이스 다이어그램
• 게임 메뉴 상황에서
• 플레이 상황에서
프로그램 구현 방법
• Asset Store 및 프리 소스를 적극 이용함
• Fbx파일을 Scene에 배치하여 맵 구현
• Player, Npc는 따로 ResourceManager를 두어 관리0
• 각각의 오브젝트의 태그를 지정(Player,Enemy,Terrain,MapObject)
실행 최종 결과물
< 시작화면
시작하기 구현 완료
플레이 방법 구현 전
게임 종료 구현 완료
옵션 선택 화면 >
볼륨 조절 구현 전
실행 최종 결과물
맵 디자인
실행 최종 결과물
플레이어는 마우스 픽킹으로 이동가능함
플레이어의 스탯이 랜덤하게 설정됨
포만감 시스템 구현완료
Npc와 일정거리 가까우면 npc는 플레이어를 예의주시함
Npc를 클릭하면 서로 마주앉아서 대화시작
대화창 UI는 ‘모여봐요 동물의 숲(닌텐도)’의 UI를 카피함
대화창 트리거, 다음 문장으로 넘어가기, 대화 종료시 대화창 없어지기 구현
첫만남 대화 일반 대화와 선택지
실행 최종 결과물
1번 선택시, 친구가 되었을 때 2번 선택시, 싸울 수 있음
3번 선택시 2번 선택시, 싸울 수 없음
실행 최종 결과물
싸움에서 이기면 랭크가 하나 오른 모습을
UI에서 확인할 수 있다.
랭크 1위가 되면 게임 클리어 화면으로 넘어간다.
코딩 내용
void Start()
{
CreateEnemy();
SetStat(stat_text_data);
SetTransform();
SetDialogue();
SetLikePoint();
}
void CreateEnemy()
{
GameObject newEnemy =
Instantiate(ResourceManager.instance.Enemy_0);
newEnemy.name = "Enemy0";
Enemy enemyScript = newEnemy.AddComponent<Enemy>();
//Enemy 컴포넌트 추가
//게임 오브젝트로 저장하는 게 아니라 스크립트로 저장
//게임 오브젝트에 접근할 때 script.gameObject로 접근하면 됨
newEnemy.AddComponent<DialogueTrigger>();
enemylist.Add(enemyScript);
//리스트로 관리
…
}
void SetTransform()
{
enemylist[0].gameObject.transform.position = new Vector3(-55,
1.53f, 66);
enemylist[0].gameObject.transform.localScale = new Vector3(0.5f,
0.5f, 0.5f);
…
}
void SetLikePoint()
{
for(int n = 0; n < 5; n++)
{
enemylist[n].likePoint = 0; //호감도 초기값 설정
enemylist[n].likePoint_max = (n + 1) * 2; //친구 달성 조건
}
}
EnemyManager.cs
ResouceManager에서 메모리에 올려놓은
Enemy들을 화면상에 가져온다.
CreateEnemy()에서
새로운 npc의 이름을 설정하고
Enemy 스크립트를 컴포넌트에 추가한다.
컴포넌트에 추가하면서
enemyScript[]라는 리스트에 각 npc의
Enemy 스크립트들을 추가한다.
리스트로 관리함으로써 각 npc의 변수에 대해
접근이 용이해진다.
SetTransform()에서
Npc 각각의 위치,크기를 설정
SetLikePoint()에서
Npc의 현재 호감도의 초기값을 0으로 설정,
친구 달성 조건인 최대 호감도를 설정한다.
(서열이 높아질수록 최대 호감도는 2씩 증가)
void SetStat(TextAsset stat_data)
{
string dialogue_text = stat_data.text; //불러온 텍스트 파일 내용
string[] lines = dialogue_text.Split('n');
int n = 0;
foreach (var line in lines)
{
Stat stat = new Stat();
if (line == "") //행이 빈 줄이면
{
continue; //반복문의 처음으로 점프
}
if (line.StartsWith("#")) //워드의 시작문자가 #이면
{
continue; //루프의 시작으로 점프
}
string[] words = line.Split();
int m = 0;
foreach(var word in words)
{
if (word == "")
continue;
//m 값을 변화시켜서 스텟 항목들을 처리한다
switch (m)
{
case 0:
stat.sociality = int.Parse(word);
break;
case 1:
stat.charisma = int.Parse(word);
break;
case 2:
stat.strength = int.Parse(word);
enemylist[n].stat = stat;
break;
} m++;
} n++;
}
}
EnemyManager.cs
SetStat()에서는
텍스트 파일을 불러와서 npc의 스텟을 설정한다.
텍스트 파일의 내용을 dialogue_text에 불러오면
# 사교성 카리스마 힘#1호 …
하나의 string으로 불러와지기 때문에
Split(‘t’)으로 한 문장씩 나눠서 lines에 저장한다.
stat에 값을 넣기 위해 foreach 반복문을 이용한다.
Lines에 있는 각 문장 중에서
#으로 시작하는 문장을 만나면 처음으로 점프
빈 문장을 만나면 처음으로 점프
그럼 line에는 다음과 같은 값이 저장된다.
10 1 3
10,1,3만 남도록 다시 Split()으로 나눈 후
foreach문을 이용한다.
m값을 변화시켜서 word값을 int형으로 변환 후
알맞은 변수에 대입한다.
m이 2일 때 npc의 스텟을 적용한다.
# 사교성 카리스마 힘
#1호
10 1 3
#2호
7 4 5
#3호
4 8 7
#4호
2 5 8
#5호
1 10 10
npcStat_data.txt
void SetDialogue()
{
for (int n = 0; n < 5; n++)
{
DialogueTrigger trigger =
enemylist[n].gameObject.GetComponent<DialogueTrigger>();
trigger.dialogue = new Dialogue(); //dialogue 초기화
trigger.dialogue.SetDialogue(dialogue_data[n]);
//텍스트 파일을 불러와서 dialogue 셋팅
trigger.dialogue.name = trigger.dialogue.sentences[0][0];
//이름 설정
trigger.isFirst = true;
trigger.isFriend = false;
trigger.isLose = false;
}
}
EnemyManager.cs
SetDialogue()에서는
대화 내용이 담긴 텍스트 파일들을 불러와
dialogue.SetDialogue()함수를 호출하고
각 npc의 dialogue를 초기화한다.
대화 내용을 구분하기 위해서
isFirst (첫만남인지)
isFriend (친구인지)
isLose (서열이 높은지) 의 초기값도 설정한다.
public class Dialogue
{
public string name; //npc이름
public string[][] sentences = new string[4][];
//대화 내용을 담을 2차원 배열
public void SetDialogue(TextAsset dialogue_data_text)
{
…
Dialogue.cs
dialogue.sentences[][] 구조
Sentences[0][]
이름
Sentences[1][]
첫만남 대화
Sentences[2][]
일반 대화
Sentences[3][]
친구 대화
[0] npc 이름
[0] 대사1
[1] 대사2
[0] 대사1
[1] 대사2
[2] 선택1 대사
[3] 선택2 대사1
[4] 선택2 대사2
[5] 선택2 대사3
[6] 선택3 대사
[0] 대사1
[1] 대사2
[2] 선택1 대사
[3] 선택2 대사1
[4] 선택2 대사2
[5] 선택2 대사3
[6] 선택3 대사
isFriend==false인 경우 대사 후 호감도 게임으로
isFriend == true인 경우 대사 후 대화 종료
선택2에서 플레이어의 서열이
1)더 낮은 경우에 카리스마 비교
false) 대사1 후 대화 종료
true) 대사2 후 싸움 씬으로
2)더 높은 경우엔 대사3 후 대화 종료
#이름
1호
#첫만남대화
안녕! 나는 1호라고 해!
앞으로 잘 부탁해!
#일반대화
또 만났네!
이번엔 어쩐일이야?
나는 오늘처럼 맑은 날이 좋아!
아직 나랑 싸울 준비가 안된 것 같은걸~?
그거 나랑 놀자는거지? 좋아!
싫어~ 싸움은 다른 애랑 해~
다음에 봐!
#친구대화
친구야! 마침 너랑 수다떨고 싶었어~
뭐 도와줄 일이라도 있는거야?
누군가에게 소중한 고양이가 된다는 건 정말 행복한 일이야!
아직 나랑 싸울 준비가 안된 것 같은걸~?
그거 나랑 놀자는거지? 좋아!
싫어~ 싸움은 다른 애랑 해~
다음에 봐!
#
No1_dialogue.txt
SetDialogue()에서는
SetStat()과 동일한 방법으로
텍스트 파일을 불러오고 한 줄씩 나눠서
lines[]에 대입한다.
Int n은 대화의 종류를 나누기 위한 변수
Int m은 문장의 순서를 구별하기 위한 변수
m값을 증가시키며
각 문장을 content[]에 하나씩 대입한다.
그러다가 #으로 시작하는 문장을 만나면
대화의 종류를 바꾸고(n++)
sentences[n]에 content를 대입한다.
public class Dialogue
{
public string name; //npc이름
public string[][] sentences = new string[4][];
public void SetDialogue(TextAsset dialogue_data_text)
{
string dialogue_text = dialogue_data_text.text;
//불러온 텍스트 파일의 내용 전체
string[] lines = dialogue_text.Split('n');
//각 행 단위로 나눠서 저장
string[] content = new string[7];
//대화 종류별로 나눔
int n = -2; //대화 종류를 나누기 위한 변수
int m = 0; //각 대화에 포함된 문장의 순서
foreach (var line in lines)
{
if (line == "") //행이 빈 줄이면
{
continue; //반복문의 처음으로 점프
}
if (line.StartsWith("#")) //워드의 시작문자가 #이면
{
n += 1; //대화의 종류를 바꿈
m = 0; //문장의 순서도 초기화
if (n >= 0) //'#이름' 이 부분을 건너뛰기 위한 조건문
{
sentences[n] = content; //대응하는 변수에 대입
content = new string[7];
}
continue; //루프의 시작으로 점프
}
content[m] = line;
m += 1;
}
}
Dialogue.cs
플레이어
필요한 변수들을 선언
public class Player : MonoBehaviour
{
//스텟 관련 변수
public string playername;
public Stat stat;
//이동 관련 변수
Animator ani;
public bool bRotate = false; //회전이 필요한지
public float fSpeed = 10f; //이동 속도
public float fAttackRange = 8f; //대화 거리
public Vector3 vEnd = Vector3.zero; //이동 목적지
public GameObject ReservedTarget = null; //대화 대상 구분
public GameObject target = null; //대화 대상
//아이템 관련 변수
private GameObject closest_item = null; //가까운 아이템
private ItemRoot item_root = null; //생명력
private Timer timer = null; //타이머
//상호작용 관련 변수
public bool isTalk = false; //대화상태인지
public int friend; //친구 수
void Start()
{
ani = GetComponent<Animator>();
float fStartTime = Random.Range(0f, 1f);
ani.Play("Idle_A", 0, fStartTime);
vEnd = transform.position;
this.item_root =
GameObject.Find("Manager").GetComponent<ItemRoot>();
this.timer = GameObject.Find("Stat").GetComponent<Timer>();
friend = 0;
}
Player.cs
시작할 때
필요한 값을 불러오거나
초기화 시켜준다.
마우스 픽킹시 이동하는 함수 구현
각각의 오브젝트를 Tag로 구별함
사과, 지형 선택시 목적지(vEnd)를 클릭한 좌표로 설정
vEnd가 null이 아니면
PlayerIdle.cs에서 Walk 애니메이션으로 전환시킴
목표점에 도달하면 PlayerWalk.cs에서
다시 PlayerIdle 상태로 전환시킴
Enemy를 선택한 경우
그 대상을 target으로 지정함
마찬가지로 target이 null이 아니면
PlayerIdle.cs에서 Walk 애니메이션으로 전환시킴
target과 ReservedTarget을 따로 두어서
각각의 오브젝트를 구분함
(enemy를 클릭했는데 이미 target에 값이 있으면
다시 새로 클릭한 enemy를 target으로 설정)
> 상호작용할 때도 중요
이외에도 클릭한 방향으로 회전하는 함수,
타겟과의 거리를 리턴해주는 함수
등이 구현되어 있다
//대화중이 아니고, UI 이외의 마우스 픽킹값이 생기면
if (Physics.Raycast(ray, out hitInfo) && !isTalk
&& !EventSystem.current.IsPointerOverGameObject())
{
switch (hitInfo.collider.tag)
{
case "Apple": //사과 선택시
case "Terrain": //지형 선택시
vEnd = hitInfo.point;
bRotate = true;
target = null;
ReservedTarget = null;
break;
case "Enemy": //적 선택시
Enemy _find =
EnemyManager.instance.GetEnemy(hitInfo.collider.name);
if (target == null)
{
target = _find.gameObject;
ReservedTarget = null;
}
//같은 enemy일 때 각각의 오브젝트를 구분
else
{
target = null;
ReservedTarget = _find.gameObject;
target = ReservedTarget;
ReservedTarget = null;
}
vEnd = _find.transform.position;
bRotate = true;
break;
default:
break;
} }
Player.cs
타겟이 null이 아니고
타겟과의 거리가 가까우면 Sit 애니메이션으로 전환
타겟이 null이 아니고
타겟과의 거리가 멀면 Walk 애니메이션으로 전환
타겟이 null이고
목표점이 현재 위치와 다르면 Walk 애니메이션으로 전환
override public void OnStateUpdate(Animator animator,
AnimatorStateInfo stateInfo, int layerIndex)
{
//enemy를 클릭했을 때
if(player.target != null)
{
//공격 범위 안에 있으면 그 방향으로 회전만
if(player.TARGETDISTANCE < player.fAttackRange)
{
player.bRotate = true;
player.Rotate(player.vEnd);
animator.SetInteger("iAniInt", 1);
}
//멀리 떨어져있으면 이동
else
{
animator.SetInteger("iAniInt", 2);
}
}
//이동해야 할 때 (나의 목적지와 현재 위치가 다르다면)
if (player.vEnd != player.transform.position &&
player.target == null)
animator.SetInteger("iAniInt", 2);
PlayerIdle.cs
override public void OnStateUpdate(Animator animator,
AnimatorStateInfo stateInfo, int layerIndex)
{
if(player.target == null)
{
player.Move();
if (player.transform.position == player.vEnd)
animator.SetInteger("iAniInt", 0);
player.Rotate(player.vEnd);
}
else if(player.target != null)
{
player.MoveTarget();
if (player.TARGETDISTANCE <= player.fAttackRange)
{
//가까워졌을 때 앉는 애니메이션으로 전환 후 대화 시작
animator.SetInteger("iAniInt", 1);
}
player.Rotate(player.vEnd);
}
}
PlayerWalk.cs
타겟이 null이면 이동
목표점이 현재 위치와 같아지면 Idle 애니메이션으로 전환
타겟이 null이 아니고
타겟과의 거리가 멀면 타겟을 향해 이동
타겟이 null이 아니고
타겟과의 거리가 가까워지면 Sit 애니메이션으로 전환
override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
if(player == null)
player = animator.GetComponent<Player>();
player.vEnd = player.transform.position;
//앉고나서 이동X
player.isTalk = true;
}
override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
//대화가 끝나면 다시 idle 상태로 돌아가기
if (player.isTalk == false)
{
animator.SetInteger("iAniInt", 0);
}
}
PlayerSit.cs
플레이어가 Sit 애니메이션 상태에 들어가면
대화가 시작되기 때문에 isTalk를 true로 설정해준다.
isTalk가 false이면 다시 idle 상태로 전환해준다.
아이템 클릭시
현재 체력이 max가 아니라면
사과를 먹는다(Eat())
먹은 사과는 다시 리스폰 시킨다
Eat()에서는
효과음을 재생하는 동안 기다렸다가
먹은 사과를 제거하고
포만감을 하나 채운다.
RespawnApple()에서는
6분 뒤 사과를 리스폰 시킨다.
void OnTriggerStay(Collider other)
{
GameObject other_go = other.gameObject;
if(other_go.layer == LayerMask.NameToLayer("Item"))
{
if(this.closest_item == null)
{
this.closest_item = other_go;
if (timer.apple < 5)
{
StartCoroutine(Eat());
StartCoroutine(RespawnApple());
}
}
}
}
…
IEnumerator Eat()
{
this.closest_item.GetComponent<AudioSource>().Play();
yield return new WaitForSeconds(0.3f);
timer.next_apple += 1;
GameObject.Destroy(this.closest_item);
this.closest_item = null;
}
IEnumerator RespawnApple()
{
yield return new WaitForSeconds(360f);
item_root.resqawnApple();
}
Player.cs
포만감 시스템
public void resqawnApple()
{
//사과 프리팹을 인스턴스화
GameObject go =
GameObject.Instantiate(applePrefab) as GameObject;
Vector3 pos =
GameObject.Find("AppleRespawn").transform.position;
pos.y = 1.8f;
pos.x += Random.Range(-1.0f, 1.0f);
pos.z += Random.Range(-1.0f, 1.0f);
go.transform.position = pos;
}
Item_root.cs
NPC
플레이어가 자신을 클릭하면
플레이어 방향으로 회전한다.
플레이어가 말을 걸었고 충분히 가까워지면
Sit 애니메이션으로 전환한다.
플레이어와 멀 때는 Idle 애니메이션으로 전환
void Update()
{
if (Player.target == this.gameObject)
{
Rotate(Player.transform.position);
target = Player;
if(Player.TARGETDISTANCE <= Player.fAttackRange)
{
//가까워졌을 때 앉는 애니메이션으로 전환 후 대화 시작
ani.SetInteger("iAniIndex", 1);
}
else
ani.SetInteger("iAniIndex", 0);
//아직 멀리있을때는 Idle 애니메이션
}
}
Enemy.cs
override public void OnStateUpdate(Animator animator,
AnimatorStateInfo stateInfo, int layerIndex)
{
if (enemy.DISTANCE <= enemy.fAttackRange)
{
enemy.vEnd = enemy.transform.position;
animator.SetInteger("iAniIndex", 0);
return;
}
if (enemy.ISMOVEEND)
{
animator.SetInteger("iAniIndex", 0);
return;
}
// Move
enemy.Move(enemy.vEnd);
// Rotate
enemy.Rotate(enemy.vEnd);
}
EnemyWalk.cs
Walk 애니메이션일 때는
목적지를 향해 회전하고 이동한다.
걷다가 목적지에 도착하면
이동을 멈추고 Idle 애니메이션으로 전환
항상 플레이어와의 거리를 확인하고
걷는 도중에 플레이어가 가까이 오면
이동을 멈추고 Idle 애니메이션으로 전환
Npc는 Idle 상태일 때도
항상 플레이어와의 거리를 확인하고
플레이어가 일정 거리 안에 들어오는 경우엔
모든 행동을 멈추고 플레이어 방향으로 회전
플레이어와 멀 때는
랜덤하게 다음 동작(iNextAni)을 결정한다
다음 동작이 이동이면
랜덤한 목적지를 받아와서 이동한다.
다음 동작이 이동이 아니면
나머지 동작으로 전환한다.
override public void OnStateUpdate(Animator animator, AnimatorStateInfo
stateInfo, int layerIndex)
{
if (enemy.DISTANCE <= enemy.fWatchRange)
{
enemy.Rotate(enemy.Player.transform.position);
//플레이어와 가까우면 플레이어 주시
iNextAni = 0;
animator.SetInteger("iAniIndex", iNextAni);
}
else
{
if ((enemy.target == null) && (enemy.DISTANCE >
enemy.fWatchRange))
//플레이어와 멀리 떨어져있고 플레이어가 말걸지 않았다면
{
int iNextAni = enemy.NextState();
if (iNextAni == 2)
{
enemy.SetMovePosition();
//걷기동작일때만 다음위치 지정
}
animator.SetInteger("iAniIndex", iNextAni);
//나머지동작으로 변경
}
}
}
EnemyIdle.cs
public void SetMovePosition()
{
float x = Random.Range(-40, 40);
float z = Random.Range(-33, 33);
vEnd.x = vSpawnPos.x + x;
vEnd.z = vSpawnPos.z + z;
}
//다음동작 결정
public int NextState()
{
int iR = Random.Range(2, 8);
// 2부터 7까지
return iR;
}
Enemy.cs
Sit 애니메이션으로 변경되면
EnemySit.cs 가 실행된다.
EnemySit.cs는 처음 실행될 때
어떤 대화를 시작할 것인지 결정하기 위해
trigger에서 bool값들을 받아온다.
isFirst가 true면 첫만남 대화
isFriend가 ture면 친구 대화
그 외의 경우엔 일반 대화를 시작한다.
isTalk가 false가 되면
대화가 끝난 것이므로 Idle 애니메이션으로 전환
Sit 애니메이션을 끝내면서
enemy의 target도 널값으로 설정해준다.
override public void OnStateEnter(Animator animator,
AnimatorStateInfo stateInfo, int layerIndex)
{
if (enemy == null)
{
enemy = animator.GetComponent<Enemy>();
}
if (trigger == null)
{
trigger = enemy.GetComponent<DialogueTrigger>();
}
player = GameObject.Find("Player").GetComponent<Player>();
if (trigger.isFirst == true)
{
trigger.TriggerDialogue(1); //첫만남 대화 시작
trigger.isFirst = false;
}
else if (trigger.isFriend == true)
trigger.TriggerDialogue(3); //친구 대화 시작
else
trigger.TriggerDialogue(2); //일반 대화 시작
}
override public void OnStateUpdate(Animator animator,
AnimatorStateInfo stateInfo, int layerIndex)
{
if (player.isTalk == false) //대화가 끝나면 일어나기
{
animator.SetInteger("iAniIndex", 0);
return;
}
// Rotate
enemy.Rotate(enemy.Player.transform.position);
}
override public void OnStateExit(Animator animator,
AnimatorStateInfo stateInfo, int layerIndex)
{
enemy.target = null; //타겟 취소
}
EnemySit.cs
public class DialogueTrigger : MonoBehaviour
{
public Dialogue dialogue;
public bool isFirst;
public bool isFriend;
public bool isLose;
public void TriggerDialogue(int n){
FindObjectOfType<DialogueManager>().StartDialogue(dialogue, n);
}
}
DialogueTrigger.cs
String형의 큐를 이용
(sentences)
DialogueTrigger에서 받은
dialogue의 문장을
하나씩 순서대로 큐에 저장
DisplayNextSentence()를 호출해
큐에 넣은 순서대로
한 문장씩 화면에 보여준다
DisplayNextSentence()에서
큐에 남은 문장이 없으면
대화를 종료한다
기본대화,친구대화의 경우
일반 대사 2개를 화면에 출력한 뒤
플레이어로부터 선택지를 기다린다.
public void StartDialogue(Dialogue dialogue, int n)
{
dialogueType = n;
dialogueBox.SetActive(true); //대화창 UI 나타내기
animator.SetBool("IsOpen", true);
sentences.Clear();
nameText.text = dialogue.name;
foreach (string sentence in dialogue.sentences[n])
{
if (sentence != null) //배열이 비어있지않다면
{
sentences.Enqueue(sentence); //큐에 한문장씩 넣는다
}
}
DisplayNextSentence(); //첫문장 출력
}
public void DisplayNextSentence()
{
if(sentences.Count == 0) //대화가 끝나면
{
animator.SetBool("IsOpen", false); //대화창 UI 사라지게하기
dialogueBox.SetActive(false);
player.isTalk = false; //대화 상태 해제
player.target = null; //타겟 해제
return;
}
if (dialogueType > 1 && sentences.Count == 6)
//기본대화, 친구대화일 경우 플레이어의 선택지를 기다려야 함
{
continueButton.SetActive(false);
selectBox.SetActive(true);
animator_select.SetBool("IsOpen", true);
}
string sentence = sentences.Dequeue();
//큐에서 한 문장만 빼서 저장한 후
StopAllCoroutines();
StartCoroutine(TypeSentence(sentence)); //UI에 나타내기
DialogueManager.cs 대화 시스템
플레이어가 대화를 선택했다면
다음 대사를 보여준다
플레이어가 싸움을 선택했다면
일단 큐에서 하나를 제거한 다음
서열이 이미 높은지 확인하고
이미 높다면 [5]를 보여주기 위해
큐에서 2개를 제거한다.
서열이 낮은데
카리스마가 부족하면 싸울 수 없다.
따라서 [3]을 보여준다.
싸울 수 있는 상태라면
[4]를 보여주기 위해 큐에서 1개를 제거
플레이어가 대화 종료를 선택했다면
[6]을 보여주기 위해
큐에서 4개를 제거한다.
public void Select1()
{
nextSentence = SelectNum.talk;
continueButton.SetActive(true);
animator_select.SetBool("IsOpen", false);
selectBox.SetActive(false);
}
public void Select2()
{
sentences.Dequeue();
if (player.target.GetComponent<DialogueTrigger>().isLose == true)
{
//서열이 이미 높으면 싸움X
sentences.Dequeue();
sentences.Dequeue();
}
else
{
//싸울 수 있는 상태
if(CompareCharisma() == true)
{
sentences.Dequeue();
}
//카리스마가 부족하면 싸움X
}
nextSentence = SelectNum.fight;
continueButton.SetActive(true);
animator_select.SetBool("IsOpen", false);
selectBox.SetActive(false);
}
public void Select3()
{
sentences.Dequeue();
sentences.Dequeue();
sentences.Dequeue();
sentences.Dequeue();
nextSentence = SelectNum.quit;
continueButton.SetActive(true);
animator_select.SetBool("IsOpen", false);
selectBox.SetActive(false);
DialogueManager.cs
[0] 대사1
[1] 대사2
[2] 선택1 대사
[3] 선택2 대사1
[4] 선택2 대사2
[5] 선택2 대사3
[6] 선택3 대사
상황에 맞는 대사를 보여준 뒤
플레이어가 대화를 선택했다면
호감도를 얻는다.
만약 친구 조건을 달성했다면
친구가 된다.
모든 고양이와 친구가 되었다면
게임이 클리어된다.
플레이어가 싸움을 선택했고
싸울 수 있는 상태라면
타겟과 플레이어의 힘을 비교해서
이기면 랭크가 오르고
진다면 아무것도 일어나지 않는다.
플레이어가 대화 종료를 선택했다면
아무일도 일어나지않는다.
if(nextSentence != SelectNum.none) //선택지에 해당하는 결과표시
{
switch (nextSentence)
{
case SelectNum.talk:
player.target.GetComponent<Enemy>().GetLikePoint(); //호감도UP
sentences.Clear(); //큐에 남은 나머지 문장들 삭제
nextSentence = SelectNum.none; //다음 대화를 위해 다시 none으로 설정
if (player.friend == 5)
StartCoroutine(ClearGame()); //모든 고양이와 친구가 되면 게임 클리어
break;
case SelectNum.fight:
sentences.Clear();
nextSentence = SelectNum.none;
//싸움 시작
if (player.target.GetComponent<DialogueTrigger>().isLose == false)
{
if(CompareCharisma() == true)
{
//continueButton.SetActive(false);
if (CompareStrength())
{
alertBox.gameObject.GetComponentInChildren<Text>().text = "랭크
가 올랐어요!";
alertBox.SetActive(true);
player.target.GetComponent<DialogueTrigger>().isLose = true;
rank.GetComponent<Rank>().RankUp();
Debug.Log("승리");
}
else
Debug.Log("패배");
}
}
break;
case SelectNum.quit:
sentences.Clear();
nextSentence = SelectNum.none;
break;
}
}
DialogueManager.cs

More Related Content

PPTX
NDC 2011 영웅전 런칭팀 박영준
PPTX
[NDC 2014] 시나리오라이터의 과거와 현재, 그리고 미래
PPTX
기획자의 포트폴리오는 어떻게 써야 할까
PDF
[PandoraCube] 게임을 재미있게 만드는 4가지 요소
PDF
전형규, 가성비 좋은 렌더링 테크닉 10선, NDC2012
PDF
게임제작개론 : #7 팀 역할과 게임 리소스에 대한 이해
PDF
장용석, fmod를이용한사운드프로그래밍, NDC2010
PDF
NDC 2015 이광영 [야생의 땅: 듀랑고] 전투 시스템 개발 일지
NDC 2011 영웅전 런칭팀 박영준
[NDC 2014] 시나리오라이터의 과거와 현재, 그리고 미래
기획자의 포트폴리오는 어떻게 써야 할까
[PandoraCube] 게임을 재미있게 만드는 4가지 요소
전형규, 가성비 좋은 렌더링 테크닉 10선, NDC2012
게임제작개론 : #7 팀 역할과 게임 리소스에 대한 이해
장용석, fmod를이용한사운드프로그래밍, NDC2010
NDC 2015 이광영 [야생의 땅: 듀랑고] 전투 시스템 개발 일지

What's hot (20)

PDF
Design de Animação
PPTX
[NDC 2021] 게임 PD가 되어 보니
PPTX
[IGC 2017] 넥슨코리아 심재근 - 시스템 기획자에 대한 기본 지식과 준비과정
PDF
NDC 2010 이은석 - 마비노기 영웅전 포스트모템 2부
PPTX
[IGC 2016] 컴투스 김동준 - 기획 지망생은 무엇을 준비하나요?
PDF
게임제작개론: #2 세부 디자인 요소
PPTX
스토리텔링과 비주얼 내러티브: 놀 치프틴은 어떻게 형님이 되었나
PDF
게임회사 실무용어 완전정복! 쿡앱스 용어정리집
PDF
게임 시스템 디자인 시작하기
PDF
NHN NEXT 게임 전공 소개
PDF
김동건, 게임팅커가 되자, 2015년 데브캣 스튜디오 워크샵
PDF
NDC 2013 이은석 - 게임 디렉터가 뭐하는 건가요
PDF
[NDC14] 모바일 게임의 다음 혁신 - 야생의 땅 듀랑고의 계산 프로세스 중심 게임 디자인
PPTX
[Unite2015 박민근] 유니티 최적화 테크닉 총정리
PPTX
뉴비라이터를 위한 게임라이팅 일반론
PDF
NDC 2015. 한 그루 한 그루 심지 않아도 돼요. 생태학에 기반한 [야생의 땅: 듀랑고]의 절차적 생성 생태계
PDF
[ NDC 14 ] 가죽 장화를 먹게 해주세요 - [ 야생의 땅 : 듀랑고 ] 의 자유도 높은 아이템 시스템 디자인
PPTX
마비노기듀얼 이야기-넥슨 김동건
PPTX
[IGC 2017] 넥슨코리아 오현근 - 평생 게임 기획자 하기
PDF
게임강연정리
Design de Animação
[NDC 2021] 게임 PD가 되어 보니
[IGC 2017] 넥슨코리아 심재근 - 시스템 기획자에 대한 기본 지식과 준비과정
NDC 2010 이은석 - 마비노기 영웅전 포스트모템 2부
[IGC 2016] 컴투스 김동준 - 기획 지망생은 무엇을 준비하나요?
게임제작개론: #2 세부 디자인 요소
스토리텔링과 비주얼 내러티브: 놀 치프틴은 어떻게 형님이 되었나
게임회사 실무용어 완전정복! 쿡앱스 용어정리집
게임 시스템 디자인 시작하기
NHN NEXT 게임 전공 소개
김동건, 게임팅커가 되자, 2015년 데브캣 스튜디오 워크샵
NDC 2013 이은석 - 게임 디렉터가 뭐하는 건가요
[NDC14] 모바일 게임의 다음 혁신 - 야생의 땅 듀랑고의 계산 프로세스 중심 게임 디자인
[Unite2015 박민근] 유니티 최적화 테크닉 총정리
뉴비라이터를 위한 게임라이팅 일반론
NDC 2015. 한 그루 한 그루 심지 않아도 돼요. 생태학에 기반한 [야생의 땅: 듀랑고]의 절차적 생성 생태계
[ NDC 14 ] 가죽 장화를 먹게 해주세요 - [ 야생의 땅 : 듀랑고 ] 의 자유도 높은 아이템 시스템 디자인
마비노기듀얼 이야기-넥슨 김동건
[IGC 2017] 넥슨코리아 오현근 - 평생 게임 기획자 하기
게임강연정리
Ad

Similar to [Unity3D] Cat RPG game (20)

PPTX
Port polio게임소개
PDF
홍성우, 내가 만든 언어로 게임 만들기, NDC2017
PPTX
Port polio게임 소개
PDF
Make a shooting game using unity
PDF
Unity 3d study #2
PDF
[NDC_16] 캐릭터 한 달에 하나씩 업데이트 하기 : '최강의 군단' 스킬 개발 툴 포스트 모템과 차기작 '건파이트 맨션' 툴 프리뷰
PDF
MMOG Server-Side 충돌 및 이동처리 설계와 구현
PDF
4조_결과보고서.pdf
PPTX
[C++ lab] 7. sud 프로젝트 구현(2)
PDF
2D RPG 개발 이론 + 티뮤리티 개발 포스트모템
PDF
유니티 UI - 텍스트, 버튼, 이미지, 씬 이동
PDF
Du room system_portfolio
PPTX
2 D게임 프로그래밍 발표 자료
PPTX
2024_INU_graduation_presentation_data.pptx
PPTX
응컴 기말 - 4주차_명승훈_임희진_지상훈.pptx
PDF
BBB
PDF
시간의 마녀(3 d)
PDF
Network programming report
PDF
Case Studies - The Role of Computer Graphics (CG) in Movie Post-production.pdf
PPT
삼국지 게임 컨셉제안서
Port polio게임소개
홍성우, 내가 만든 언어로 게임 만들기, NDC2017
Port polio게임 소개
Make a shooting game using unity
Unity 3d study #2
[NDC_16] 캐릭터 한 달에 하나씩 업데이트 하기 : '최강의 군단' 스킬 개발 툴 포스트 모템과 차기작 '건파이트 맨션' 툴 프리뷰
MMOG Server-Side 충돌 및 이동처리 설계와 구현
4조_결과보고서.pdf
[C++ lab] 7. sud 프로젝트 구현(2)
2D RPG 개발 이론 + 티뮤리티 개발 포스트모템
유니티 UI - 텍스트, 버튼, 이미지, 씬 이동
Du room system_portfolio
2 D게임 프로그래밍 발표 자료
2024_INU_graduation_presentation_data.pptx
응컴 기말 - 4주차_명승훈_임희진_지상훈.pptx
BBB
시간의 마녀(3 d)
Network programming report
Case Studies - The Role of Computer Graphics (CG) in Movie Post-production.pdf
삼국지 게임 컨셉제안서
Ad

[Unity3D] Cat RPG game

  • 1. 졸업프로젝트 개발 최종 발표 : Unity 엔진을 이용한 고양이 3D RPG 게임 제작 정보미디어학과 16038020 김희진
  • 2. 프로젝트 개요 -길고양이로 태어나 혹독한 길바닥 생활에서 살아남는 고양이 rpg게임 -게임 목표: 서열 1위와 싸워서 이긴다. -게임 진행: 스텟을 높여 자신 보다 높은 서열의 고양이들을 차례차례 쓰러트린다. -사용 프로그램: 유니티(C#) 개념도 3D 파일 관리 지형 배치 Npc, 아이템 배치 애니메이션 설정 스크립트 관리 스크립트 작성 게임오브젝트 관리 행동 제어 스텟 제어 상호작용 제어 마우스/게임 패드 입력
  • 3. 플레이어 다른 고양이와 상호작용 • 서열 싸움 > 이기면 서열 상승 • 대화 > 친구가 됨 >서열 싸움 서포트 엔딩 • 서열1위 • … 기타 사물과 상호작용 • 스텟 증가 • 스텟 감소 게임의 전반적인 흐름도
  • 4. 프로젝트 시나리오 @캐릭터 설정 : 랜덤하게 스텟이 정해진다. (1~5 사이, 미모만 1~10사이, 모든 스텟의 최대치는 10) *몸집 - 힘 세짐(몸싸움에서 이길 수 있음) *목소리 - 자싞감 높아짐(기싸움에서 이길 수 있음) *사교성 - 친구 잘 사귈 수 있음(친구랑 다니면 싸울 때 서포트해줌) *미모 - 인기가 많아진다(인간들이 공물 바쳐줌) 캐릭터는 배부름 게이지가 있다. (최대 10. 5분에 1씩 깎임) 배부름을 채우기 위해서는 캣맘이 주는 밥을 먹거나 음식물 쓰레기를 뒤져야 한다. 음식물 쓰레기를 찾거나 캣맘이 어디에 밥을 주는지 찾아야 함 배부름 10일 때는 힘+1 버프가 지속된다. 캐릭터는 UI에서 자싞의 서열 순위를 알 수 있다. 서열 1위 고양이는 몸집10목소리10사교성1미모1인 폭군 고양이다. 서열 1위 고양이와 친구가 되려면 사교성이 10이어야 한다.
  • 5. 프로젝트 시나리오 캐릭터는 다른 고양이와 상호작용 할 수 있다 1. 대화 - 대화를 통해서 다른 고양이와의 호감도를 쌓을 수 있다. 사교성이 높은 고 양이는 친구 고양이를 잘 사귈 수 있다. 친구가 된 고양이는 함께 다닐 수 있 다. 친구 고양이는 서열 싸움시 서포트 해준다. 2. 서열 싸움 - 서열 싸움은 2단계로 이루어진다. 1단계(기싸움) : 기싸움은 목소리 차이가 2 이상이면 싸움을 걸 수 없다. 2단계(몸싸움) : 단순히 힘이 쎈 고양이가 이긴다. 단, 친구 고양이가 서포트 할 경우 힘 버프 (친구 고양이의 힘에 따라서 버프도 세짐) - 서열 싸움에서 이기면 서열(레벨)이 1 올라감 + 몸집 1 커짐 @이벤트 - 지나가는 인간에게 다가가면 공물을 받을 수도 있다. 공물 종류에 따라 몸 집 or 목소리를 높여주거나 배부름 게이지를 채워준다. (크레이지아케이드 의 비행기 처럼) - 고양이 친구가 2마리 늘어날 때마다 사교성 +1
  • 6. 프로젝트 시나리오 @엔딩 : 1. 서열 1위 고양이와 서열 싸움에서 이기면 보통 엔딩 2. 서열 1위 고양이와 친구가 되면 평화 엔딩 3. 처음 스텟 중 미모가 만렙이면 냥줍 엔딩 4. 배부름 게이지가 0이 되면 배드 엔딩... 5. 친구 없이 모든 고양이를 이기면 파괴왕 고양이! 히든 엔딩
  • 7. 생김새 이름 1호 2호 3호 4호 5호 서열 Lv 1 Lv 2 Lv 3 Lv 4 Lv 5 성격 순함 중2병 퉁명스러움 내성적 폭군 힘 3 5 7 8 10 카리스마 1 4 8 5 10 사교성 10 7 4 2 1 npc 개별컨셉
  • 8. 유스케이스 다이어그램 • 게임 메뉴 상황에서 • 플레이 상황에서
  • 9. 프로그램 구현 방법 • Asset Store 및 프리 소스를 적극 이용함 • Fbx파일을 Scene에 배치하여 맵 구현 • Player, Npc는 따로 ResourceManager를 두어 관리0 • 각각의 오브젝트의 태그를 지정(Player,Enemy,Terrain,MapObject)
  • 10. 실행 최종 결과물 < 시작화면 시작하기 구현 완료 플레이 방법 구현 전 게임 종료 구현 완료 옵션 선택 화면 > 볼륨 조절 구현 전
  • 12. 실행 최종 결과물 플레이어는 마우스 픽킹으로 이동가능함 플레이어의 스탯이 랜덤하게 설정됨 포만감 시스템 구현완료 Npc와 일정거리 가까우면 npc는 플레이어를 예의주시함 Npc를 클릭하면 서로 마주앉아서 대화시작
  • 13. 대화창 UI는 ‘모여봐요 동물의 숲(닌텐도)’의 UI를 카피함 대화창 트리거, 다음 문장으로 넘어가기, 대화 종료시 대화창 없어지기 구현 첫만남 대화 일반 대화와 선택지 실행 최종 결과물
  • 14. 1번 선택시, 친구가 되었을 때 2번 선택시, 싸울 수 있음 3번 선택시 2번 선택시, 싸울 수 없음
  • 15. 실행 최종 결과물 싸움에서 이기면 랭크가 하나 오른 모습을 UI에서 확인할 수 있다. 랭크 1위가 되면 게임 클리어 화면으로 넘어간다.
  • 17. void Start() { CreateEnemy(); SetStat(stat_text_data); SetTransform(); SetDialogue(); SetLikePoint(); } void CreateEnemy() { GameObject newEnemy = Instantiate(ResourceManager.instance.Enemy_0); newEnemy.name = "Enemy0"; Enemy enemyScript = newEnemy.AddComponent<Enemy>(); //Enemy 컴포넌트 추가 //게임 오브젝트로 저장하는 게 아니라 스크립트로 저장 //게임 오브젝트에 접근할 때 script.gameObject로 접근하면 됨 newEnemy.AddComponent<DialogueTrigger>(); enemylist.Add(enemyScript); //리스트로 관리 … } void SetTransform() { enemylist[0].gameObject.transform.position = new Vector3(-55, 1.53f, 66); enemylist[0].gameObject.transform.localScale = new Vector3(0.5f, 0.5f, 0.5f); … } void SetLikePoint() { for(int n = 0; n < 5; n++) { enemylist[n].likePoint = 0; //호감도 초기값 설정 enemylist[n].likePoint_max = (n + 1) * 2; //친구 달성 조건 } } EnemyManager.cs ResouceManager에서 메모리에 올려놓은 Enemy들을 화면상에 가져온다. CreateEnemy()에서 새로운 npc의 이름을 설정하고 Enemy 스크립트를 컴포넌트에 추가한다. 컴포넌트에 추가하면서 enemyScript[]라는 리스트에 각 npc의 Enemy 스크립트들을 추가한다. 리스트로 관리함으로써 각 npc의 변수에 대해 접근이 용이해진다. SetTransform()에서 Npc 각각의 위치,크기를 설정 SetLikePoint()에서 Npc의 현재 호감도의 초기값을 0으로 설정, 친구 달성 조건인 최대 호감도를 설정한다. (서열이 높아질수록 최대 호감도는 2씩 증가)
  • 18. void SetStat(TextAsset stat_data) { string dialogue_text = stat_data.text; //불러온 텍스트 파일 내용 string[] lines = dialogue_text.Split('n'); int n = 0; foreach (var line in lines) { Stat stat = new Stat(); if (line == "") //행이 빈 줄이면 { continue; //반복문의 처음으로 점프 } if (line.StartsWith("#")) //워드의 시작문자가 #이면 { continue; //루프의 시작으로 점프 } string[] words = line.Split(); int m = 0; foreach(var word in words) { if (word == "") continue; //m 값을 변화시켜서 스텟 항목들을 처리한다 switch (m) { case 0: stat.sociality = int.Parse(word); break; case 1: stat.charisma = int.Parse(word); break; case 2: stat.strength = int.Parse(word); enemylist[n].stat = stat; break; } m++; } n++; } } EnemyManager.cs SetStat()에서는 텍스트 파일을 불러와서 npc의 스텟을 설정한다. 텍스트 파일의 내용을 dialogue_text에 불러오면 # 사교성 카리스마 힘#1호 … 하나의 string으로 불러와지기 때문에 Split(‘t’)으로 한 문장씩 나눠서 lines에 저장한다. stat에 값을 넣기 위해 foreach 반복문을 이용한다. Lines에 있는 각 문장 중에서 #으로 시작하는 문장을 만나면 처음으로 점프 빈 문장을 만나면 처음으로 점프 그럼 line에는 다음과 같은 값이 저장된다. 10 1 3 10,1,3만 남도록 다시 Split()으로 나눈 후 foreach문을 이용한다. m값을 변화시켜서 word값을 int형으로 변환 후 알맞은 변수에 대입한다. m이 2일 때 npc의 스텟을 적용한다. # 사교성 카리스마 힘 #1호 10 1 3 #2호 7 4 5 #3호 4 8 7 #4호 2 5 8 #5호 1 10 10 npcStat_data.txt
  • 19. void SetDialogue() { for (int n = 0; n < 5; n++) { DialogueTrigger trigger = enemylist[n].gameObject.GetComponent<DialogueTrigger>(); trigger.dialogue = new Dialogue(); //dialogue 초기화 trigger.dialogue.SetDialogue(dialogue_data[n]); //텍스트 파일을 불러와서 dialogue 셋팅 trigger.dialogue.name = trigger.dialogue.sentences[0][0]; //이름 설정 trigger.isFirst = true; trigger.isFriend = false; trigger.isLose = false; } } EnemyManager.cs SetDialogue()에서는 대화 내용이 담긴 텍스트 파일들을 불러와 dialogue.SetDialogue()함수를 호출하고 각 npc의 dialogue를 초기화한다. 대화 내용을 구분하기 위해서 isFirst (첫만남인지) isFriend (친구인지) isLose (서열이 높은지) 의 초기값도 설정한다. public class Dialogue { public string name; //npc이름 public string[][] sentences = new string[4][]; //대화 내용을 담을 2차원 배열 public void SetDialogue(TextAsset dialogue_data_text) { … Dialogue.cs
  • 20. dialogue.sentences[][] 구조 Sentences[0][] 이름 Sentences[1][] 첫만남 대화 Sentences[2][] 일반 대화 Sentences[3][] 친구 대화 [0] npc 이름 [0] 대사1 [1] 대사2 [0] 대사1 [1] 대사2 [2] 선택1 대사 [3] 선택2 대사1 [4] 선택2 대사2 [5] 선택2 대사3 [6] 선택3 대사 [0] 대사1 [1] 대사2 [2] 선택1 대사 [3] 선택2 대사1 [4] 선택2 대사2 [5] 선택2 대사3 [6] 선택3 대사 isFriend==false인 경우 대사 후 호감도 게임으로 isFriend == true인 경우 대사 후 대화 종료 선택2에서 플레이어의 서열이 1)더 낮은 경우에 카리스마 비교 false) 대사1 후 대화 종료 true) 대사2 후 싸움 씬으로 2)더 높은 경우엔 대사3 후 대화 종료 #이름 1호 #첫만남대화 안녕! 나는 1호라고 해! 앞으로 잘 부탁해! #일반대화 또 만났네! 이번엔 어쩐일이야? 나는 오늘처럼 맑은 날이 좋아! 아직 나랑 싸울 준비가 안된 것 같은걸~? 그거 나랑 놀자는거지? 좋아! 싫어~ 싸움은 다른 애랑 해~ 다음에 봐! #친구대화 친구야! 마침 너랑 수다떨고 싶었어~ 뭐 도와줄 일이라도 있는거야? 누군가에게 소중한 고양이가 된다는 건 정말 행복한 일이야! 아직 나랑 싸울 준비가 안된 것 같은걸~? 그거 나랑 놀자는거지? 좋아! 싫어~ 싸움은 다른 애랑 해~ 다음에 봐! # No1_dialogue.txt
  • 21. SetDialogue()에서는 SetStat()과 동일한 방법으로 텍스트 파일을 불러오고 한 줄씩 나눠서 lines[]에 대입한다. Int n은 대화의 종류를 나누기 위한 변수 Int m은 문장의 순서를 구별하기 위한 변수 m값을 증가시키며 각 문장을 content[]에 하나씩 대입한다. 그러다가 #으로 시작하는 문장을 만나면 대화의 종류를 바꾸고(n++) sentences[n]에 content를 대입한다. public class Dialogue { public string name; //npc이름 public string[][] sentences = new string[4][]; public void SetDialogue(TextAsset dialogue_data_text) { string dialogue_text = dialogue_data_text.text; //불러온 텍스트 파일의 내용 전체 string[] lines = dialogue_text.Split('n'); //각 행 단위로 나눠서 저장 string[] content = new string[7]; //대화 종류별로 나눔 int n = -2; //대화 종류를 나누기 위한 변수 int m = 0; //각 대화에 포함된 문장의 순서 foreach (var line in lines) { if (line == "") //행이 빈 줄이면 { continue; //반복문의 처음으로 점프 } if (line.StartsWith("#")) //워드의 시작문자가 #이면 { n += 1; //대화의 종류를 바꿈 m = 0; //문장의 순서도 초기화 if (n >= 0) //'#이름' 이 부분을 건너뛰기 위한 조건문 { sentences[n] = content; //대응하는 변수에 대입 content = new string[7]; } continue; //루프의 시작으로 점프 } content[m] = line; m += 1; } } Dialogue.cs
  • 23. 필요한 변수들을 선언 public class Player : MonoBehaviour { //스텟 관련 변수 public string playername; public Stat stat; //이동 관련 변수 Animator ani; public bool bRotate = false; //회전이 필요한지 public float fSpeed = 10f; //이동 속도 public float fAttackRange = 8f; //대화 거리 public Vector3 vEnd = Vector3.zero; //이동 목적지 public GameObject ReservedTarget = null; //대화 대상 구분 public GameObject target = null; //대화 대상 //아이템 관련 변수 private GameObject closest_item = null; //가까운 아이템 private ItemRoot item_root = null; //생명력 private Timer timer = null; //타이머 //상호작용 관련 변수 public bool isTalk = false; //대화상태인지 public int friend; //친구 수 void Start() { ani = GetComponent<Animator>(); float fStartTime = Random.Range(0f, 1f); ani.Play("Idle_A", 0, fStartTime); vEnd = transform.position; this.item_root = GameObject.Find("Manager").GetComponent<ItemRoot>(); this.timer = GameObject.Find("Stat").GetComponent<Timer>(); friend = 0; } Player.cs 시작할 때 필요한 값을 불러오거나 초기화 시켜준다.
  • 24. 마우스 픽킹시 이동하는 함수 구현 각각의 오브젝트를 Tag로 구별함 사과, 지형 선택시 목적지(vEnd)를 클릭한 좌표로 설정 vEnd가 null이 아니면 PlayerIdle.cs에서 Walk 애니메이션으로 전환시킴 목표점에 도달하면 PlayerWalk.cs에서 다시 PlayerIdle 상태로 전환시킴 Enemy를 선택한 경우 그 대상을 target으로 지정함 마찬가지로 target이 null이 아니면 PlayerIdle.cs에서 Walk 애니메이션으로 전환시킴 target과 ReservedTarget을 따로 두어서 각각의 오브젝트를 구분함 (enemy를 클릭했는데 이미 target에 값이 있으면 다시 새로 클릭한 enemy를 target으로 설정) > 상호작용할 때도 중요 이외에도 클릭한 방향으로 회전하는 함수, 타겟과의 거리를 리턴해주는 함수 등이 구현되어 있다 //대화중이 아니고, UI 이외의 마우스 픽킹값이 생기면 if (Physics.Raycast(ray, out hitInfo) && !isTalk && !EventSystem.current.IsPointerOverGameObject()) { switch (hitInfo.collider.tag) { case "Apple": //사과 선택시 case "Terrain": //지형 선택시 vEnd = hitInfo.point; bRotate = true; target = null; ReservedTarget = null; break; case "Enemy": //적 선택시 Enemy _find = EnemyManager.instance.GetEnemy(hitInfo.collider.name); if (target == null) { target = _find.gameObject; ReservedTarget = null; } //같은 enemy일 때 각각의 오브젝트를 구분 else { target = null; ReservedTarget = _find.gameObject; target = ReservedTarget; ReservedTarget = null; } vEnd = _find.transform.position; bRotate = true; break; default: break; } } Player.cs
  • 25. 타겟이 null이 아니고 타겟과의 거리가 가까우면 Sit 애니메이션으로 전환 타겟이 null이 아니고 타겟과의 거리가 멀면 Walk 애니메이션으로 전환 타겟이 null이고 목표점이 현재 위치와 다르면 Walk 애니메이션으로 전환 override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { //enemy를 클릭했을 때 if(player.target != null) { //공격 범위 안에 있으면 그 방향으로 회전만 if(player.TARGETDISTANCE < player.fAttackRange) { player.bRotate = true; player.Rotate(player.vEnd); animator.SetInteger("iAniInt", 1); } //멀리 떨어져있으면 이동 else { animator.SetInteger("iAniInt", 2); } } //이동해야 할 때 (나의 목적지와 현재 위치가 다르다면) if (player.vEnd != player.transform.position && player.target == null) animator.SetInteger("iAniInt", 2); PlayerIdle.cs override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { if(player.target == null) { player.Move(); if (player.transform.position == player.vEnd) animator.SetInteger("iAniInt", 0); player.Rotate(player.vEnd); } else if(player.target != null) { player.MoveTarget(); if (player.TARGETDISTANCE <= player.fAttackRange) { //가까워졌을 때 앉는 애니메이션으로 전환 후 대화 시작 animator.SetInteger("iAniInt", 1); } player.Rotate(player.vEnd); } } PlayerWalk.cs 타겟이 null이면 이동 목표점이 현재 위치와 같아지면 Idle 애니메이션으로 전환 타겟이 null이 아니고 타겟과의 거리가 멀면 타겟을 향해 이동 타겟이 null이 아니고 타겟과의 거리가 가까워지면 Sit 애니메이션으로 전환
  • 26. override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { if(player == null) player = animator.GetComponent<Player>(); player.vEnd = player.transform.position; //앉고나서 이동X player.isTalk = true; } override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { //대화가 끝나면 다시 idle 상태로 돌아가기 if (player.isTalk == false) { animator.SetInteger("iAniInt", 0); } } PlayerSit.cs 플레이어가 Sit 애니메이션 상태에 들어가면 대화가 시작되기 때문에 isTalk를 true로 설정해준다. isTalk가 false이면 다시 idle 상태로 전환해준다.
  • 27. 아이템 클릭시 현재 체력이 max가 아니라면 사과를 먹는다(Eat()) 먹은 사과는 다시 리스폰 시킨다 Eat()에서는 효과음을 재생하는 동안 기다렸다가 먹은 사과를 제거하고 포만감을 하나 채운다. RespawnApple()에서는 6분 뒤 사과를 리스폰 시킨다. void OnTriggerStay(Collider other) { GameObject other_go = other.gameObject; if(other_go.layer == LayerMask.NameToLayer("Item")) { if(this.closest_item == null) { this.closest_item = other_go; if (timer.apple < 5) { StartCoroutine(Eat()); StartCoroutine(RespawnApple()); } } } } … IEnumerator Eat() { this.closest_item.GetComponent<AudioSource>().Play(); yield return new WaitForSeconds(0.3f); timer.next_apple += 1; GameObject.Destroy(this.closest_item); this.closest_item = null; } IEnumerator RespawnApple() { yield return new WaitForSeconds(360f); item_root.resqawnApple(); } Player.cs 포만감 시스템 public void resqawnApple() { //사과 프리팹을 인스턴스화 GameObject go = GameObject.Instantiate(applePrefab) as GameObject; Vector3 pos = GameObject.Find("AppleRespawn").transform.position; pos.y = 1.8f; pos.x += Random.Range(-1.0f, 1.0f); pos.z += Random.Range(-1.0f, 1.0f); go.transform.position = pos; } Item_root.cs
  • 28. NPC
  • 29. 플레이어가 자신을 클릭하면 플레이어 방향으로 회전한다. 플레이어가 말을 걸었고 충분히 가까워지면 Sit 애니메이션으로 전환한다. 플레이어와 멀 때는 Idle 애니메이션으로 전환 void Update() { if (Player.target == this.gameObject) { Rotate(Player.transform.position); target = Player; if(Player.TARGETDISTANCE <= Player.fAttackRange) { //가까워졌을 때 앉는 애니메이션으로 전환 후 대화 시작 ani.SetInteger("iAniIndex", 1); } else ani.SetInteger("iAniIndex", 0); //아직 멀리있을때는 Idle 애니메이션 } } Enemy.cs override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { if (enemy.DISTANCE <= enemy.fAttackRange) { enemy.vEnd = enemy.transform.position; animator.SetInteger("iAniIndex", 0); return; } if (enemy.ISMOVEEND) { animator.SetInteger("iAniIndex", 0); return; } // Move enemy.Move(enemy.vEnd); // Rotate enemy.Rotate(enemy.vEnd); } EnemyWalk.cs Walk 애니메이션일 때는 목적지를 향해 회전하고 이동한다. 걷다가 목적지에 도착하면 이동을 멈추고 Idle 애니메이션으로 전환 항상 플레이어와의 거리를 확인하고 걷는 도중에 플레이어가 가까이 오면 이동을 멈추고 Idle 애니메이션으로 전환
  • 30. Npc는 Idle 상태일 때도 항상 플레이어와의 거리를 확인하고 플레이어가 일정 거리 안에 들어오는 경우엔 모든 행동을 멈추고 플레이어 방향으로 회전 플레이어와 멀 때는 랜덤하게 다음 동작(iNextAni)을 결정한다 다음 동작이 이동이면 랜덤한 목적지를 받아와서 이동한다. 다음 동작이 이동이 아니면 나머지 동작으로 전환한다. override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { if (enemy.DISTANCE <= enemy.fWatchRange) { enemy.Rotate(enemy.Player.transform.position); //플레이어와 가까우면 플레이어 주시 iNextAni = 0; animator.SetInteger("iAniIndex", iNextAni); } else { if ((enemy.target == null) && (enemy.DISTANCE > enemy.fWatchRange)) //플레이어와 멀리 떨어져있고 플레이어가 말걸지 않았다면 { int iNextAni = enemy.NextState(); if (iNextAni == 2) { enemy.SetMovePosition(); //걷기동작일때만 다음위치 지정 } animator.SetInteger("iAniIndex", iNextAni); //나머지동작으로 변경 } } } EnemyIdle.cs public void SetMovePosition() { float x = Random.Range(-40, 40); float z = Random.Range(-33, 33); vEnd.x = vSpawnPos.x + x; vEnd.z = vSpawnPos.z + z; } //다음동작 결정 public int NextState() { int iR = Random.Range(2, 8); // 2부터 7까지 return iR; } Enemy.cs
  • 31. Sit 애니메이션으로 변경되면 EnemySit.cs 가 실행된다. EnemySit.cs는 처음 실행될 때 어떤 대화를 시작할 것인지 결정하기 위해 trigger에서 bool값들을 받아온다. isFirst가 true면 첫만남 대화 isFriend가 ture면 친구 대화 그 외의 경우엔 일반 대화를 시작한다. isTalk가 false가 되면 대화가 끝난 것이므로 Idle 애니메이션으로 전환 Sit 애니메이션을 끝내면서 enemy의 target도 널값으로 설정해준다. override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { if (enemy == null) { enemy = animator.GetComponent<Enemy>(); } if (trigger == null) { trigger = enemy.GetComponent<DialogueTrigger>(); } player = GameObject.Find("Player").GetComponent<Player>(); if (trigger.isFirst == true) { trigger.TriggerDialogue(1); //첫만남 대화 시작 trigger.isFirst = false; } else if (trigger.isFriend == true) trigger.TriggerDialogue(3); //친구 대화 시작 else trigger.TriggerDialogue(2); //일반 대화 시작 } override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { if (player.isTalk == false) //대화가 끝나면 일어나기 { animator.SetInteger("iAniIndex", 0); return; } // Rotate enemy.Rotate(enemy.Player.transform.position); } override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { enemy.target = null; //타겟 취소 } EnemySit.cs public class DialogueTrigger : MonoBehaviour { public Dialogue dialogue; public bool isFirst; public bool isFriend; public bool isLose; public void TriggerDialogue(int n){ FindObjectOfType<DialogueManager>().StartDialogue(dialogue, n); } } DialogueTrigger.cs
  • 32. String형의 큐를 이용 (sentences) DialogueTrigger에서 받은 dialogue의 문장을 하나씩 순서대로 큐에 저장 DisplayNextSentence()를 호출해 큐에 넣은 순서대로 한 문장씩 화면에 보여준다 DisplayNextSentence()에서 큐에 남은 문장이 없으면 대화를 종료한다 기본대화,친구대화의 경우 일반 대사 2개를 화면에 출력한 뒤 플레이어로부터 선택지를 기다린다. public void StartDialogue(Dialogue dialogue, int n) { dialogueType = n; dialogueBox.SetActive(true); //대화창 UI 나타내기 animator.SetBool("IsOpen", true); sentences.Clear(); nameText.text = dialogue.name; foreach (string sentence in dialogue.sentences[n]) { if (sentence != null) //배열이 비어있지않다면 { sentences.Enqueue(sentence); //큐에 한문장씩 넣는다 } } DisplayNextSentence(); //첫문장 출력 } public void DisplayNextSentence() { if(sentences.Count == 0) //대화가 끝나면 { animator.SetBool("IsOpen", false); //대화창 UI 사라지게하기 dialogueBox.SetActive(false); player.isTalk = false; //대화 상태 해제 player.target = null; //타겟 해제 return; } if (dialogueType > 1 && sentences.Count == 6) //기본대화, 친구대화일 경우 플레이어의 선택지를 기다려야 함 { continueButton.SetActive(false); selectBox.SetActive(true); animator_select.SetBool("IsOpen", true); } string sentence = sentences.Dequeue(); //큐에서 한 문장만 빼서 저장한 후 StopAllCoroutines(); StartCoroutine(TypeSentence(sentence)); //UI에 나타내기 DialogueManager.cs 대화 시스템
  • 33. 플레이어가 대화를 선택했다면 다음 대사를 보여준다 플레이어가 싸움을 선택했다면 일단 큐에서 하나를 제거한 다음 서열이 이미 높은지 확인하고 이미 높다면 [5]를 보여주기 위해 큐에서 2개를 제거한다. 서열이 낮은데 카리스마가 부족하면 싸울 수 없다. 따라서 [3]을 보여준다. 싸울 수 있는 상태라면 [4]를 보여주기 위해 큐에서 1개를 제거 플레이어가 대화 종료를 선택했다면 [6]을 보여주기 위해 큐에서 4개를 제거한다. public void Select1() { nextSentence = SelectNum.talk; continueButton.SetActive(true); animator_select.SetBool("IsOpen", false); selectBox.SetActive(false); } public void Select2() { sentences.Dequeue(); if (player.target.GetComponent<DialogueTrigger>().isLose == true) { //서열이 이미 높으면 싸움X sentences.Dequeue(); sentences.Dequeue(); } else { //싸울 수 있는 상태 if(CompareCharisma() == true) { sentences.Dequeue(); } //카리스마가 부족하면 싸움X } nextSentence = SelectNum.fight; continueButton.SetActive(true); animator_select.SetBool("IsOpen", false); selectBox.SetActive(false); } public void Select3() { sentences.Dequeue(); sentences.Dequeue(); sentences.Dequeue(); sentences.Dequeue(); nextSentence = SelectNum.quit; continueButton.SetActive(true); animator_select.SetBool("IsOpen", false); selectBox.SetActive(false); DialogueManager.cs [0] 대사1 [1] 대사2 [2] 선택1 대사 [3] 선택2 대사1 [4] 선택2 대사2 [5] 선택2 대사3 [6] 선택3 대사
  • 34. 상황에 맞는 대사를 보여준 뒤 플레이어가 대화를 선택했다면 호감도를 얻는다. 만약 친구 조건을 달성했다면 친구가 된다. 모든 고양이와 친구가 되었다면 게임이 클리어된다. 플레이어가 싸움을 선택했고 싸울 수 있는 상태라면 타겟과 플레이어의 힘을 비교해서 이기면 랭크가 오르고 진다면 아무것도 일어나지 않는다. 플레이어가 대화 종료를 선택했다면 아무일도 일어나지않는다. if(nextSentence != SelectNum.none) //선택지에 해당하는 결과표시 { switch (nextSentence) { case SelectNum.talk: player.target.GetComponent<Enemy>().GetLikePoint(); //호감도UP sentences.Clear(); //큐에 남은 나머지 문장들 삭제 nextSentence = SelectNum.none; //다음 대화를 위해 다시 none으로 설정 if (player.friend == 5) StartCoroutine(ClearGame()); //모든 고양이와 친구가 되면 게임 클리어 break; case SelectNum.fight: sentences.Clear(); nextSentence = SelectNum.none; //싸움 시작 if (player.target.GetComponent<DialogueTrigger>().isLose == false) { if(CompareCharisma() == true) { //continueButton.SetActive(false); if (CompareStrength()) { alertBox.gameObject.GetComponentInChildren<Text>().text = "랭크 가 올랐어요!"; alertBox.SetActive(true); player.target.GetComponent<DialogueTrigger>().isLose = true; rank.GetComponent<Rank>().RankUp(); Debug.Log("승리"); } else Debug.Log("패배"); } } break; case SelectNum.quit: sentences.Clear(); nextSentence = SelectNum.none; break; } } DialogueManager.cs