대전 과제

2020. 5. 12. 04:31Unity/과제

1. 구조 정리

 

a. Lobby에서 Button 2개 생성. 각각 캐릭터 선택의 개념

 

Button 클릭 시 LoadScene이 이루어지므로, 해당 코드 직전에 sceneLoaded를 이용하여 Lobby Scene이 종료되기 전, 다음 Scene에 넘길 정보를 해당 연결 메소드에서 처리한다.

여기서 넘길 정보는 다음 Scene의 GameObject에 붙어있는 Script 안의 Method를 사용해서 그 매개변수로 넘길 것이기 때문에, 이전 Scene과 다음 Scene이 동시에 살아있는 순간이 필요함. 그 작업이 sceneLoaded에서 이루어지는 것.

 

이번 과제의 경우, InGame 내의 ConfirmPlayer 메소드의 매개변수로, Lobby에서 내가 어떤 캐릭터를 선택했는지에 대한 정보값을 ID로 넘김.

 

다음 Scene의 메소드에 접근하는 방법은, GameObject.FindObjectOfType<Type>(); 을 이용하여 다음 Scene의 객체? GameObject를 반환받아 접근하면 된다.

 

b. 다음 Scene인 InGame에서 구조를 잡은 순서는 다음과 같다.

 

1) 이전 Scene(Lobby)에서 넘겨받은 ID값을 이용하여 Prefab을 Instantiate하는 것.

여기서 주의할 점은, Hero라는 별개의 부모 GameObject를 같이 생성하여 그 안에 실제 Model을 집어넣고, 다룰 때에는 부모 GameObject를 이용하여 컨트롤하도록 구성하는 것.

 

2) Player와 Enemy를 구별하여 생성하도록 구조를 짜고, 서로가 서로를 알 수 있도록 서로의 GameObject를 건네받는    모양을 생각하는 것.

사실 이게 제일 까다로웠고, 코드도 맘에 들지 않는다.

이유인 즉슨, 서로의 GameObject를 전달하려면 양측의 GameObject에 동시에 접근할 수 있는 순간이 있어야 하는데, 내가 구성한 코드는 CreateCharacter를 Player와 Enemy가 각각 따로 타서 생성할 수 있도록 짰기 때문.

따라서 SelectPlayer를 별도로 구성해서 그곳에서 서로 데이터를 교환하고, 역할을 구분짓도록 구성했다.

근데 메소드명은 Select. 즉 플레이어를 선택한다는 의미. 따라서 메소드명과 역할이 맞지 않는다.

나눠보려고 했으나 구조를 잡아뜯는수밖에 답이 안보여서 그건 시간상 무리이므로 건너뛴다.

 

3) 경기의 시작을 컨트롤하는 Button 생성

이후 내부에서는 시작점인 Run()만 두 캐릭터에게 실행하도록 한다. 어짜피 나머지 행위들은 Event로 처리할 것.

 

4) 그 이후 Start()에서 대리자 이벤트 추가. 모든 행위의 시작점은 Hero Character 각자가 Event로 활동하도록 연결.

이 시점에서 대략적으로 Hero 내부에서 해당 Event가 호출될 상황이 그려져야 이 부분을 짤 수 있다.

 

5) 나머지는 승리자 출력용 Text UI 연결....이거 사이즈 이쁘게 조절하는법 잘 모르겠다. 나중에 시간날때 찾아보자.

 

c. Hero Character가 취하는 행동을 기준으로 분리하여 구성.

 

1) InGame에서 썼던 ID는 당연히 여기로 들어온다. 데미지나 Distance, Direction 등 서로의 데이터를 참조해야 할

상황이 많기 때문에, Hero Script 내에서도 GameObject를 선언하여 저장해놓고 써야 한다.

 

2) Initialize() 중요하다. 상호작용할 데이터를 먼저 정리하지 않고, 상호작용부터 구현하다가 나중에 디버깅만 2시간동안 삽질했다. 항상 데이터먼저 정리하자. 정리는 DataManager의 싱글톤 패턴으로 쉽게 가져다 쓰면 됨.

 

3) Update() 에서의 Character의 행동 구조 짜기. Switch - case에 Enum을 활용해서 정리하니까 훨씬 쉽다.

"남현석" 님의 코드에서 영감을 얻음. 앞으로는 이렇게 하자.

여기서 "Done = -1" 같은거 하나 더 추가해서, 게임 종료 후에도 Update 반복 호출을 막는것도 고려하면 편함.

 

4) Attack() 같은 반복 행동 시에 Timer 꼭 쓰자. 이거 안쓰면 공격 모션 하는데에 걸리는 시간보다 더 빠르게 같은 공격 모션 명령이 프레임단위로 들어가는데 막을 방법이 없다. 모션 다씹히고 첫 한방만 때리게 됨.

 

5) transform.Translate(Vector)는 "상대좌표 기준"으로의 이동이다. 진짜 중요한 부분이다.

이게 무슨말이냐면 Character 간 서로에게 접근하기위해 Direction을 산출할 때 상대 Position에서 내 Position을 뺀 단위벡터를 자신의 Direction으로 사용하면 될 줄 알았는데, 사실 이건 World 좌표계. 즉 절대좌표 기준이고, Translate는 상대좌표 기준이기때문에 결과적으로 (-)에 (-)를 곱한 개념이 되어서 둘 다 같은 방향으로 가버린다. 백날 디버깅 해봐야 어짜피 Direction은 정상적으로 서로 반대방향으로 나오기 때문에 함수 자체를 이해하지 못하면 이거 해결 안 됨.

코드에도 주석을 달아놨지만, 방법은 Space.World를 두번째 매개변수로 넣어주면 된다. 이러면 절대좌표로 계산됨.

       

 

 

 2. UnityFile

 

 
 
 

3. Script Code

<Hero.cs>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Assertions.Must;
using UnityEngine.Events;
 
public class Hero : MonoBehaviour
{
    public enum eState
    {
        Done = -1,
        Run,
        Idle,
        Attack,
        Die
    }
    private eState estate;
 
    // json
    private string heroName;
    private int hp;
    private int damage;
 
    public GameObject model;
    public GameObject target;
    
    private Animation anim;
    private float timer;
    public UnityAction onArriveAction;
    public UnityAction onDieAction;
 
    private void Start()
    {
 
    }
    private void Update()
    {
        switch (this.estate)
        {
            case eState.Run:
                var dir = (this.target.transform.position - this.transform.position).normalized;
                // Space.World를 넣지 않으면 단위벡터의 반대값에서 상대좌표로 계산되기 때문에
                // 마이너스에 마이너스를 곱한 개념이 되어서
                // 결국 두 캐릭터가 같은 방향으로 뛰게 됨.
                // 따라서 Space.World를 넣어서 월드 좌표계로 바꾸어야 함.
                this.transform.Translate(dir * 1f * Time.deltaTime, Space.World);
                if (Vector3.Distance(this.gameObject.transform.position, target.transform.position) <= 0.5f)
                {
                    this.onArriveAction();
                }
 
                break;
            case eState.Idle:
                break;
            case eState.Attack:
                this.timer += Time.deltaTime;       // 타이머 시작
                if(this.timer >= this.anim["attack_sword_01"].length+0.1f) // 3) 타이머에서 흐른 시간이 공격 모션 시간보다 충분히 지났다면 다시 시작
                {
                    Hero targetComponent = this.target.GetComponent<Hero>();
                    targetComponent.hp -= this.damage;
                    if (targetComponent.hp <= 0)
                    {
                        targetComponent.onDieAction();      // 경기가 끝나면
                        this.estate = eState.Done;          // case를 Default로 바꿔서 다른 State를 타지 못하게 함
                    }
                    this.anim.Play("attack_sword_01");   // 1) 공격 (공격 모션 시간 Start)
                    this.timer = 0f;                    // 2) 공격 후 Timer = 0
                }
                break;
            case eState.Die:
                break;
            default:
                break;
            
        }
 
    }
    public void Initialize(int id, GameObject model)
    {
        var data = DataManager.GetInstance().GetCharacterById(id);
        this.estate = eState.Idle;
        this.heroName = data.name;
        this.hp = data.hp;
        this.damage = data.damage;
        this.model = model;
        this.anim = this.model.GetComponent<Animation>();        
    }
    public void Run()
    {
        this.anim.Play("run@loop");
        this.estate = eState.Run;
    }
    public void Idle()
    {
        this.anim.Play("idle@loop");
        this.estate = eState.Attack;
    }
    public void Attack()
    {
        this.estate = eState.Attack;
        this.timer = 1f;
    }
    public void Die()
    {
        this.anim.Play("die");
        this.estate = eState.Die;
    }    
    
}
cs

 

<InGame.cs>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
 
public class InGame : MonoBehaviour
{
 
    public Button btn;
    public Hero player;
    public Hero enemy;
    public Text alarm;
 
    void Start()
    {
       this.btn.onClick.AddListener(() =>
        {
            this.player.Run();
            this.enemy.Run();            
        });
        this.player.onArriveAction += () =>
        {
            this.player.Attack();
        };
        this.enemy.onArriveAction += () =>
        {
            this.enemy.Attack();
        };
        this.player.onDieAction += () =>
        {
            this.alarm.text = $"승자 : Player";
            this.player.Die();
            this.enemy.Idle();
            this.alarm.gameObject.SetActive(true);
        };
        this.enemy.onDieAction += () =>
        {
            this.alarm.text = $"승자 : Enemy";
            this.enemy.Die();
            this.player.Idle();
            this.alarm.gameObject.SetActive(true);
        };
 
    }
    public Hero CreateCharacter(int characterId, Vector3 position, bool isPlayer)
    {
        var data = DataManager.GetInstance().GetCharacterById(characterId);
 
        var modelPath = string.Format("Prefabs/{0}", data.res_name);
        var modelPrefab = Resources.Load<GameObject>(modelPath);
        var modelGo = Instantiate<GameObject>(modelPrefab);
 
        var heroPrefab = Resources.Load<GameObject>("Prefabs/Hero");
        var heroGo = Instantiate<GameObject>(heroPrefab);
 
        var hero = heroGo.AddComponent<Hero>();
        modelGo.transform.SetParent(heroGo.transform);
 
        // Position & hero Initialize Setting
        if (isPlayer == true)        // Player일 때
        {
            heroGo.transform.position = Vector3.zero;
            hero.Initialize(characterId, modelGo);
        }
        if (isPlayer == false)      // enemy일 때
        {
            heroGo.transform.position = new Vector3(005);
            heroGo.transform.rotation = Quaternion.Euler(new Vector3(01800));
            hero.Initialize(characterId, modelGo);
        }
        return hero;       
    }
    public void SelectPlayer(int id)
    {
        if (id == 100)
        {
            this.player = CreateCharacter(100, Vector3.zero, true);
            this.enemy = CreateCharacter(101new Vector3(005), false);
 
            this.player.target = this.enemy.gameObject;
            this.enemy.target = this.player.gameObject;           
        }
        else if (id == 101)
        {
            this.player = CreateCharacter(101, Vector3.zero, true);
            this.enemy = CreateCharacter(100new Vector3(005), false);
 
            this.player.target = this.enemy.gameObject;
            this.enemy.target = this.player.gameObject;
        }
    }
}
cs

 

<Lobby.cs>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
 
public class Lobby : MonoBehaviour
{
    public Button player100Btn;
    public Button player101Btn;
    public int characterId;
    // Start is called before the first frame update
    void Start()
    {
        DataManager.GetInstance().LoadDatas();
        this.player100Btn.onClick.AddListener(() =>
        {
            SceneManager.sceneLoaded += ConfirmPlayer100;
            SceneManager.LoadScene("InGame");
        });
        this.player101Btn.onClick.AddListener(() =>
        {
            SceneManager.sceneLoaded += ConfirmPlayer101;
            SceneManager.LoadScene("InGame");
 
        });
    }
    private void ConfirmPlayer100(Scene arg0, LoadSceneMode arg1)
    {
        var inGame = GameObject.FindObjectOfType<InGame>();
        inGame.SelectPlayer(100);
    }
    private void ConfirmPlayer101(Scene arg0, LoadSceneMode arg1)
    {
        var inGame = GameObject.FindObjectOfType<InGame>();
        inGame.SelectPlayer(101);
    }
    
 
    // Update is called once per frame
    void Update()
    {
        
    }
}
cs

4. 되짚어보기

 

- 이벤트와 대리자에 대한 개념이 제대로 잡혀있지 않아서 구조 자체를 짜는데에 너무 오래걸림.

  힘들게 짠 구조도 네다섯번은 뒤엎다보면 도중에 내가 뭘 하고있는지 기억이 안 날 정도.

  근데 안된다고 스트레스 받을 건 없다. 어짜피 조진만큼 기억에 오래 남는다.

 

- 적당히 하다가 안되면 던지는 습관을 들이려고 계속 다짐했는데, 또 옛날 습관이 나와버렸다.

  이렇게 밤새 하고 다음날 수업 1시간만 몽롱하게 들으면 그거 복구하는데에 개별복습 4시간 걸린다.

  안 좋은 습관이다. 다음부턴 알람 맞춰놓고 과제하자.

 

- 지속성이 중요하다.

  나보다 잘하는 사람만 쳐다보면서 비교하기 시작하면, 더 열심히 하면서 실력은 빨리 늘 수 있겠지만

  스트레스받고, 체력분배 안되서 수업 놓치고, 금방 흥미를 잃어버리고, 주변을 놓치고, 하여튼 단점이 더 많다.

 

  남하고 비교하면서 스스로가 깎이지 말자.

  우연찮게 내 주변에 나보다 못하는 사람이 많다고 거만해져서 놀기만 할 수도 없고.

  우연찮게 내 주변에 나보다 잘하는 사람이 많다고 항상 스트레스만 받을 수도 없다.

  비교는 어제의 나 자신하고만 하면 된다. 걔보다만 잘하면 됐음.

'Unity > 과제' 카테고리의 다른 글

달리기 시합 게임 과제 (미완성)  (0) 2020.05.11