포스트

섹션13. 미니 RPG - 1

위 글은 인프런에 있는 Rookiss님의 [C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] Part3: 유니티 엔진 강의를 듣고 남긴 필기입니다.

환경 세팅

  • Terrain Component
    • Terrain Layer를 Texture2D 파일을 이용해 만들 수 있음 ⇒ Paint Texture로 Terrain에 Texture를 입히는 것이 가능.
  • Light Component
    • Mode
      • Realtime
        • 그림자와 빛 연산을 실시간으로. 연산량 높음. 간접 조명 효과는 없음.
      • Baked
        • Unity 에디터에서 베이크된 조명에 대한 계산을 수행하고, 결과를 조명 데이터로 디스크에 저장.
        • 런타임 시 셰이딩 비용을 줄이고 그림자 렌더링 비용을 줄임.
        • 동적인 게임 오브젝트는 이와 상호작용 불가.
      • Mixed
        • 반반

이동

  • NavMesh ⇒ 건물 위를 올라가는 걸 막는 방법으로 사용
    • Window > AI > Navigation (Obsolete) 을 통해 Bake 가능
    • 플레이어에는 NavAgent Component를 부착.
    • NavAgentMove()를 이용하는 방식으로 움직임 코드를 수정.
    • NavMesh는 Static 오브젝트를 고려해서 움직일 수 있도록 길을 만든다

      (= Static이 아닌 동적인 오브젝트는 무시하고 움직이도록 메쉬를 만든다)


    • 건물과 같은 오브젝트와 맞닿아 길이 막혔을 때, 계속 달리는 모션을 막기 위해서
      • 플레이어로부터 움직이는 방향으로 짧은 길이의 Raycast로 건물이 감지되면 움직임을 멈추는 방법으로 수정.

스텟

플레이어 캐릭터인 UnityChan과, 몬스터가 될 DeathKnight. 일전에 무료로 받음.

  • 이전에 만든 Data.Content.csData라는 이름의 namespace에 넣어주고, 이 안의 Stat클래스와 StatData클래스를 사용하는 곳도 모두 Data.StatData.StatData로 변경
  • Content라는 이름의 폴더를 만든다. 여기에 유닛의 스텟을 관리하는 스크립트를 만들 예정.

  • 내부에 Stat.csPlayerStat.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
public class Stat : MonoBehaviour
{
    [SerializeField]
    protected int _level;
    [SerializeField]
    protected int _hp;
    [SerializeField]
    protected int _maxHp;
    
    [SerializeField]
    protected int _attack;
    [SerializeField]
    protected int _defense;
    
    [SerializeField]
    protected float _moveSpeed;

    public int Level
    {
        get => _level;
        set => _level = value;
    }
    public int Hp
    {
        get => _hp;
        set => _hp = value;
    }
    public int MaxHp
    {
        get => _maxHp;
        set => _maxHp = value;
        
    }
    
    public int Attack
    {
        get => _attack;
        set => _attack = value;
    }
    public int Defense
    {
        get => _defense;
        set => _defense = value;
    }
    
    public float MoveSpeed
    {
        get => _moveSpeed;
        set => _moveSpeed = value;
    }

    private void Start()
    {
        // 임시값
        _level = 1;
        _hp = 100;
        _maxHp = 100;
        _attack = 10;
        _defense = 5;
        _moveSpeed = 5.0f;
    }
}
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
public class PlayerStat : Stat
{
    [SerializeField] 
    protected int _exp;
    [SerializeField] 
    protected int _gold;

    public int Exp
    {
        get => _exp;
        set => _exp = value;
    }

    public int Gold
    {
        get => _gold;
        set => _gold = value;
    }
    
    private void Start()
    {
        // 임시값
        _level = 1;
        _hp = 100;
        _maxHp = 100;
        _attack = 10;
        _defense = 5;
        _moveSpeed = 5.0f;
        _exp = 0;
        _gold = 0;
    }
}
  • 각각은 몬스터, 그리고 플레이어 캐릭터의 정보를 저장하는 스크립트.
  • 플레이어 캐릭터 프리팹과, 몬스터 프리팹에 각각을 부착해준다.
  • 이제 이 클래스들을 사용해야하기 때문에, 우선 PlayerController.cs 내부에서 자체적으로 만들어 사용하던 _speed 대신 스스로에게 부착된 PlayerStat.cs_moveSpeed 를 활용하도록 변경

상황에 따라 변하는 마우스 커서

  • 몬스터를 Monster 레이어로 변경.
  • PlayerController.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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
public class PlayerController : MonoBehaviour
{
    private PlayerStat _stat;    // 위에서 만든 PlayerStat을 활용하기 위해 추가
    
    private NavMeshAgent navMeshAgent;
    private Vector3 _destPos;

    private Texture2D _attackIcon;    // 몬스터 위에 올라가있을 때의 마우스 커서 아이콘
    private Texture2D _handIcon;      // 맨 바닥 위에 올라가있을 때의 마우스 커서 아이콘

    enum CursorType                   // 마우스 커서의 상태를 저장할 enum
    {
        None,
        Attack,
        Hand,
    }

    private CursorType _cursorType = CursorType.None;    // 마우스 커서의 상태를 저장할 enum 변수
    
    void Start()
    {
				// 마우스 커서 아이콘을 미리 로드
        _attackIcon = Managers.Resource.Load<Texture2D>("Textures/Cursor/Attack");
        _handIcon = Managers.Resource.Load<Texture2D>("Textures/Cursor/Hand");
        
        // 기존의 _speed 변수 대신, PlayerStat을 이용한다.
        _stat = GetComponent<PlayerStat>();
        
        // InputManager에서 Event 함수를 실행하도록 맡긴다.
        Managers.Input.MouseAction -= OnMouseClicked;
        Managers.Input.MouseAction += OnMouseClicked;

        navMeshAgent = gameObject.GetOrAddComponent<NavMeshAgent>();
    }

    public enum PlayerState
    {
        Die,
        Moving,
        Idle,
        Skill,
    }

    private PlayerState _state = PlayerState.Idle;

    void UpdateDie()
    {
        // 아무것도 못한다.
    }
    
    void UpdateMoving()
    {
        Vector3 dir = _destPos - transform.position;
        if (dir.magnitude < 0.1f)   // 거리는 float, 이렇게 매우 적은 오차에 든다면 도착한것으로 간주.
        {
            _state = PlayerState.Idle;
        }   
        else    // 그렇지 않다면, 아직 도착하지 않은것이니 이동하도록.
        {
            float moveDist = Mathf.Clamp(_stat.MoveSpeed * Time.deltaTime, 0, dir.magnitude);
            navMeshAgent.Move(moveDist * dir.normalized);
            
            Debug.DrawRay(transform.position + Vector3.up * 0.5f, dir.normalized, Color.green);
            if(Physics.Raycast(transform.position, dir, 1, LayerMask.GetMask("Block"))) 
            {
                _state = PlayerState.Idle;
                return;
            }
            
            transform.rotation =
                Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), 10 * Time.deltaTime);    // 도착지를 바라보도록.
        }
        
        // 애니메이션
        Animator anim = GetComponent<Animator>();
        // 현재 게임 상태에 대한 정보를 넘겨준다
        anim.SetFloat("speed", _stat.MoveSpeed);
    }
    
    void UpdateIdle()
    {
        // 애니메이션
        Animator anim = GetComponent<Animator>();
        anim.SetFloat("speed", 0);
    }
    
    private void Update()
    {
        UpdateMouseCursor();
        
        switch (_state)
        {
            case PlayerState.Die:
                UpdateDie();
                break;
            case PlayerState.Moving:
                UpdateMoving();
                break;
            case PlayerState.Idle:
                UpdateIdle();
                break;
        }
    }

		// 마우스의 위치를 실시간으로 감시하며 커서를 변화시키는 함수
    void UpdateMouseCursor()
    {
        var ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        Debug.DrawRay(Camera.main.transform.position, ray.direction * 100.0f, Color.red,1.0f);
        
        if (Physics.Raycast(ray, out var hit, 100.0f, _mask))
        {
            if (hit.collider.gameObject.layer == (int)Define.Layer.Monster)
            {
                if (_cursorType != CursorType.Attack)
                {
                    // 벡터는 커서의 좌측상단을 맞추기 위한 값
                    Cursor.SetCursor(_attackIcon, new Vector2(_attackIcon.width / 5, 0), CursorMode.Auto);
                    _cursorType = CursorType.Attack;
                }
            }
            else
            {
                if (_cursorType != CursorType.Hand)
                {
                    // 벡터는 커서의 좌측상단을 맞추기 위한 값
                    Cursor.SetCursor(_handIcon, new Vector2(_handIcon.width / 3, 0), CursorMode.Auto);
                    _cursorType = CursorType.Hand;
                }
            }
        }
    }
    
    // 마우스 클릭에 반응 할 레이어만 마스킹
		// 땅의 레이어는 Ground로 수정.
    private int _mask = (1 << (int)Define.Layer.Ground) | (1 << (int)Define.Layer.Monster);
    
    void OnMouseClicked(Define.MouseEvent mouseEvent)
    {
        if (_state == PlayerState.Die) return;
        
        var ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        //Debug.DrawRay(Camera.main.transform.position, ray.direction * 100.0f, Color.red,1.0f);
        
        if (Physics.Raycast(ray, out var hit, 100.0f, _mask))
        {
            _destPos = hit.point;
            _state = PlayerState.Moving;

            if (hit.collider.gameObject.layer == (int)Define.Layer.Monster)
            {
                Debug.Log("Monster Click!");
            }
            else
            {
                Debug.Log("Ground Click!");
            }
        }
    }
}
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.