본문으로 바로가기

요마전의 개발 이슈 중 하나인 "유저가 맘대로 팬을 그은 궤적에 맞게 스킬이 발동되어야 한다." 를 구현해봅시다. 미리 조사해놓은 자료들에 따르면, Unity에서는 라인을 그리는 방법이 다양합니다. 크게 세가지로 나뉘는데 다음과 같은 방법이 있습니다.


  - LineRenderer 를 통한 구현방법

  - mesh Graphic 을 통한 구현방법

  - GL Class 를 통한 구현방법


각 각의 방법은 장/단점이 존재하는데, LineRenderer 의 경우 구현은 쉬우나 퍼포먼스가 떨어지는 방식이라고 하고, 두번째인 mesh Graphic은 텍스쳐나 쉐이더 등을 계속 생성해서 구현하는 방식으로 코드상으로 복잡하지만 LineRenderer에 비해 퍼포먼스가 좋다고 합니다. 그리고 마지막 GL Class 방법은 오직 Unity Pro버전에서만 가능한 구현방법으로 코드의 가독성과 성능에 있어서 앞선 두방법 보다 앞선다고 합니다.
(참고 : http://www.everyday3d.com/blog/index.php/2010/03/15/3-ways-to-draw-3d-lines-in-unity3d/)


따라서 우리는 간편하기도 하고 퍼포먼스가 좋다는 GL Class방식을 사용해보려 시도해보려했으나 예제코드에 대한 이해가 부족해서 고민하던 찰나, "후르츠 닌자"라는 게임을 알게되었고 이 게임에서 사용되는 터치궤적에 대한 효과를 따라 구현해보게 되었습니다.

결국 LineRenderer를 통한 구현방법을 사용하게 된 셈입니다. 단, "후르츠 닌자"의 경우 2D카메라의 형태를 사용하고 있고, 저희 프로젝트에서는 3D카메라를 사용해야 했기에 아래 그림과 같이 구현하게 되었습니다.


구현방법을 보면 3인칭 카메라로 보이는 화면영역에서 캐릭터의 움직임과 스킬액션이 동시에 이루어져야 하는데, 이때 스킬을 시전할땐 화면에서 보이는 깊이(원근)가 달라도 모두 스킬이 적용될 수 있도록 해야합니다. 즉, 그리는 선은 평면이어도 깊이가 모두 반영이 될 수 있도록 Ray를 이용하여 구현하여야 했습니다.

그림에서 보이는 초록색과 빨간색 화살표는 Ray로 우선 먼저 스킬액션의 선을 저장하고, 그 선을 효과로 보여주는 배열이 필요합니다. 또한 스킬에 닿을 대상 오브젝트들을 효과를 보여줄때 동시에 Ray를 쏴서 효과를 적용시킬 배열이 필요합니다. 따라서 구현시에 2개의 배열을 사용하여 구현하였습니다.


기본적인 캐릭터 이동부분이 구현되어 있었기 때문에, 코드에서는 스킬버튼을 누를때 캐릭터의 움직임을 멈추고 스킬을 시전할 수 있도록 상태를 나누게 됩니다. 구현된 코드는 아래와 같으며, 정리되지 않은 코드이므로 참고만 하시길 바랍니다. :)


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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
 
public class SkillAction : MonoBehaviour
{
 
    /* 변수 설명부분
     * [GameObject] player : 플레이어 캐릭터
     * [GameObject] gb : 불꽃이펙트 (Trail Renderer)
     * [GameObject] gb2 : 선 이펙트 (Trail Renderer)
     * [GameObject] g : 안쓰는 오브젝트1
     * [Vector3 배열] lp : 
     * [Vector3 배열] lp2 :
     * s : Vector3 형 변수 (초기값은 자동으로 0, 0, 0)
     * s2 : Vector3 형 변수 (초기값은 자동으로 0, 0, 0)
     * aa : 스킬버튼 눌렀을때(-1), 보통상태(0), 이펙트가움직이는동안(1)
     * time : 이펙트가 그려질때 사용되는 시간변수
     * time2 : 초기화부분에서 사용되는 시간변수
     * time3 : 불꽃이펙트의 시작에 사용되는 시간변수
     * a : 배열 위치변수
     * bb : 이펙트가 스킬을 그린지점까지 도착여부를 알리는 스테이트
     */
    
    public GameObject player;
    public GameObject gb;
    public GameObject gb2;
    public GameObject g;
    private Vector3[] lp;
    private Vector3[] lp2;
    private Vector3 s;
    private Vector3 s2;
    static public int aa = 0;
    float time = 0;
    float time2 = 0;
    float time3 = 0;
    int a = 0;
    int bb = 0;
    private bool monster_collider;
 
 
    void Start()
    {
 
        lp = new Vector3[0];
        lp2 = new Vector3[0];
        //Debug.Log(s);
        //Debug.Log(s2);
 
    }
 
 
    void Update()
    {
 
        // 스킬버튼을 눌렀을 시
        if (aa == -1)
        {
            // 불꽃이펙트의 시작위치를 플레이어의 위치로 설정
            gb.transform.position = player.transform.position;
 
            time3 += Time.deltaTime;
 
            // 버튼 누르고 그리기까지 0.5초
            if (time3 > 0.5f)
            {
                time3 = 0;
                aa = 0;
 
            }
        }
        
 
        // 캐릭터의 움직임 스테이트가 정지상태일때 (스킬시전때문에)
        if (CharacterRayMove.cha_state == 1)
        {
 
            if (aa == 0)
            {
                // 마우스가 드래그 상태이면(버튼클릭이 유지되는 상태)
                if (Input.GetMouseButton(0))
                {
                    // 선 이펙트의 움직임을 인풋의 경로로 받아온다.
                    gb2.transform.position = GetNewPoint();
 
                    // e를 카메라 좌표로 받아옴
                    Vector3 e = GetNewPoint();
 
                    // e2를 마우스 좌표로 받아옴
                    Vector3 e2 = GetNewPoint2();
 
                    // 받아온 s 좌표가 0이 아니면.. (처음이 아닌이상 대부분 거침..)
                    if (s != Vector3.zero)
                    {
                        // lp 배열에 좌표추가(카메라)
                        lp = AddLine(lp, s, e, false);
                    }
 
                    // 받아온 s2 좌표가 0이 아니면..
                    if (s2 != Vector3.zero)
                    {
                        // lp2 배열에 좌표추가(마우스)
                        lp2 = AddLine(lp2, s2, e2, false);
                    }
 
                    // s2 변수에 e2 변수의 값(인풋포지션)을 대입
                    s2 = e2;
 
                    // s 변수에 월드좌표로 변환한 e 변수의 값(*,*,10)을 대입
                    s = e;
                }
                else
                {
                    s2 = Vector3.zero;
                    s = Vector3.zero;
                }
 
                // 마우스 드래그가 끝나는 순간 = 마우스 버튼클릭이 끝나는 순간
                if (Input.GetMouseButtonUp(0))
                {
                    // 상태를 이펙트가 움직이는 동안의 상태로 바꿈 
                    aa = 1;
                }
 
            }
 
            // 이펙트가 움직이는 동안의 상태
            if (aa == 1)
            {
                time += Time.deltaTime;
 
                // 이펙트랑 lp배열0번째 거리 비교해서 1.0 이상이면(거리가 있으면) 이펙트를 그려줌.
                if (Vector3.Distance(gb.transform.position, lp[0]) > 1.0f)
                {
                    Vector3 vec = lp[0] - player.transform.position;
                    // 이펙트를 움직이는 부분
                    gb.transform.Translate(vec * 0.08f);
                    //                    Debug.Log(Vector3.Distance(gb.transform.position, lp[0]));
 
                }
                else
                {
                    // 이펙트가 플레이어위치에서 스킬을 시전한 시작위치에 도달하면 상태를 바꿔줌
                    bb = 1;
                }
 
                // lp 0번배열에 도착했으니 이펙트를 그려라.
                if (bb == 1)
                {
                    // 시간이 0.01초 지나면
                    if (time > 0.01f)
                    {
                        // lp배열의 크기가 a(??)보다 클 경우
                        if (lp.Length > a)
                        {
                            // 이펙트의 위치를 lp배열의 a 위치로 잡는다.
                            gb.transform.position = lp[a];
 
                            // Ray ray2는 카메라에서 쏘는 것으로
                            Ray ray2 = Camera.main.ScreenPointToRay(lp2[a]);
                            
                            // RaycastHit 판별변수(hitInfo2) 선언
                            RaycastHit hitInfo2;
 
                            // 화면상에 레이를 표시(Debug용)
                            Debug.DrawRay(ray2.origin, ray2.direction * 100, Color.yellow);
 
                            // ray2가 쏴진 정보가 hitInfo2에 저장
                            if (Physics.Raycast(ray2, out hitInfo2))
                            {
                                Debug.Log(hitInfo2.collider.tag);
 
                                // Ray를 충돌한 오브젝트 정보가 "MONSTER"이면 해당오브젝트를 소멸
                                if (hitInfo2.collider.tag == "MONSTER")
                                {
                                    Debug.Log("!!!!!");
                                    Destroy(hitInfo2.collider.transform.gameObject);
                                }
                            }
 
                            time = 0;
                            a++;
                        }
                        else
                        {
                            // 스킬을 다 그렸으므로 초기화상태로 돌림
                            bb = 0;
                            aa = 2;
                        }
                    }
                }
            }
 
            // 스킬 이펙트 그려진 이후 초기화 부분
            if (aa == 2)
            {
                time2 += Time.deltaTime;
 
                if (time2 > 1.5f)
                {
                    lp = new Vector3[0];
                    lp2 = new Vector3[0];
                    aa = 0;
                    a = 0;
                    Debug.Log("change");
                    CharacterRayMove.cha_state = 0;
                }
            }
        }
    }
 
    /* 배열에 점을 추가하는 함수 (s = start, e = end, tmp = true/false) */
    Vector3[] AddLine(Vector3[] l, Vector3 s, Vector3 e, bool tmp)
    {
        int vl = l.Length;
 
        if (!tmp || vl == 0)
        {
            l = resizeVertices(l, 2);
        }
        else
        {
            vl -= 2;
        }
 
        l[vl] = s;
        l[vl + 1] = e;
 
        return l;
        
    }
 
    /* 배열을 받아서 배열이 꽉 차면 늘리는 함수 */
    Vector3[] resizeVertices(Vector3[] ovs, int ns)
    {
        Vector3[] nvs = new Vector3[ovs.Length + ns];
 
        for (int i = 0; i < ovs.Length; i++)
        {
            nvs[i] = ovs[i];
        }
 
        return nvs;
    }
 
 
    /* 인풋(마우스)좌표를 월드좌표로 변환해서 리턴하는 함수 */
    Vector3 GetNewPoint()
    {
      
        return Camera.main.ScreenToWorldPoint(
            new Vector3(Input.mousePosition.x, Input.mousePosition.y, 10));
 
        
    }
 
    /* 인풋(마우스)좌표를 리턴하는 함수 */
    Vector3 GetNewPoint2()
    {
        return Input.mousePosition;
    }
 
}