Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
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
Archives
Today
Total
관리 메뉴

codingfarm

4. 2D 플랫포머 - 오르막길 본문

Unity5/기타

4. 2D 플랫포머 - 오르막길

scarecrow1992 2020. 7. 29. 23:58

지금까지 만든 이동코드는 평지를 이동하는데엔 아무 이상도 없다.

하지만 오르막길을 오를때는 이동이 불가능할 정도로 속도가 급격히 떨어지는것을 확인 할 수 있다.

문제 해결을 위해선 원인을 알아야한다.

먼저 캐릭터가 이동하는 순간의 모습을 캡처해보자

이동방향으로 horizontalRayCount개의 ray가 생성되어 해당 프레임동안 캐릭터의 이동 거리(velocity.x + deltaTime)에 skinWidth를 더한 길이만큼의 범위내에서 장애물을 감지하는 방식이다.

ray자체가 skinWidth만큼 캐릭터의 테두리로부터 안쪽에서 발사되는 방식이기에 아주작은 턱은  넘지 못하는 모습을 볼 수 있다.

턱이 높아서 캐릭터가 못지나간다.
턱이 너무 낮아서 캐릭터가 지나갈 수 있다.

 

이와 마찬가지로 경사가 매우 낮다면 ray가 장애물을 감지 할 수 없으므로 큰 어려움 없이 지나갈 수 있을것이다.

 

1도로 기울어진 초록색 경사면을 큰 무리 없이 지나간다.
30도로 기울어진 경사면도 속도 저하는 있지만 잘 올라간다.

 

그렇다면 제일 아래의 ray에 걸릴정도로 가파른 경사를 만나면 어떻게 될까?

ray가 경사면을 감지하게 되어 더이상 경사면을 탈 수 없게 되었다.

 

즉, 플레이어의 ray들 중 하나라도 obstacle이 감지되면 무조건 "장애물"로 인식하므로 이동 할 수 없게 되는것이다.

그렇기에 경사면을 감지하기 위한 추가적인 조치가 필요하다.

이 문제를 해결하기 위해 수정된 Controller2D 코드를  살펴보자

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
using UnityEngine;
using System.Collections;
 
[RequireComponent (typeof (BoxCollider2D))]
public class Controller2D : MonoBehaviour {
 
    public LayerMask collisionMask;
 
    const float skinWidth = .015f;
    public int horizontalRayCount = 4;
    public int verticalRayCount = 4;
 
    float maxClimbAngle = 80;
 
    float horizontalRaySpacing;
    float verticalRaySpacing;
 
    BoxCollider2D collider;
    RaycastOrigins raycastOrigins;
    public CollisionInfo collisions;
 
    void Start() {
        collider = GetComponent<BoxCollider2D> ();
        CalculateRaySpacing ();
    }
 
    public void Move(Vector3 velocity) {
        UpdateRaycastOrigins ();
        collisions.Reset ();
 
        if (velocity.x != 0) {
            HorizontalCollisions (ref velocity);
        }
        if (velocity.y != 0) {
            VerticalCollisions (ref velocity);
        }
 
        transform.Translate (velocity);
    }
 
    void HorizontalCollisions(ref Vector3 velocity) {
        float directionX = Mathf.Sign (velocity.x);
        float rayLength = Mathf.Abs (velocity.x) + skinWidth;
        
        for (int i = 0; i < horizontalRayCount; i ++) {
            Vector2 rayOrigin = (directionX == -1)?raycastOrigins.bottomLeft:raycastOrigins.bottomRight;
            rayOrigin += Vector2.up * (horizontalRaySpacing * i);
            RaycastHit2D hit = Physics2D.Raycast(rayOrigin, Vector2.right * directionX, rayLength, collisionMask);
 
            Debug.DrawRay(rayOrigin, Vector2.right * directionX * rayLength,Color.red);
 
            if (hit) {
 
                float slopeAngle = Vector2.Angle(hit.normal, Vector2.up);
 
                if (i == 0 && slopeAngle <= maxClimbAngle) {
                    float distanceToSlopeStart = 0;
                    if (slopeAngle != collisions.slopeAngleOld) {
                        distanceToSlopeStart = hit.distance-skinWidth;
                        velocity.x -= distanceToSlopeStart * directionX;
                    }
                    ClimbSlope(ref velocity, slopeAngle);
                    velocity.x += distanceToSlopeStart * directionX;
                }
 
                if (!collisions.climbingSlope || slopeAngle > maxClimbAngle) {
                    velocity.x = (hit.distance - skinWidth) * directionX;
                    rayLength = hit.distance;
 
                    if (collisions.climbingSlope) {
                        velocity.y = Mathf.Tan(collisions.slopeAngle * Mathf.Deg2Rad) * Mathf.Abs(velocity.x);
                    }
 
                    collisions.left = directionX == -1;
                    collisions.right = directionX == 1;
                }
            }
        }
    }
    
    void VerticalCollisions(ref Vector3 velocity) {
        float directionY = Mathf.Sign (velocity.y);
        float rayLength = Mathf.Abs (velocity.y) + skinWidth;
 
        for (int i = 0; i < verticalRayCount; i ++) {
            Vector2 rayOrigin = (directionY == -1)?raycastOrigins.bottomLeft:raycastOrigins.topLeft;
            rayOrigin += Vector2.right * (verticalRaySpacing * i + velocity.x);
            RaycastHit2D hit = Physics2D.Raycast(rayOrigin, Vector2.up * directionY, rayLength, collisionMask);
 
            Debug.DrawRay(rayOrigin, Vector2.up * directionY * rayLength,Color.red);
 
            if (hit) {
                velocity.y = (hit.distance - skinWidth) * directionY;
                rayLength = hit.distance;
 
                if (collisions.climbingSlope) {
                    velocity.x = velocity.y / Mathf.Tan(collisions.slopeAngle * Mathf.Deg2Rad) * Mathf.Sign(velocity.x);
                }
 
                collisions.below = directionY == -1;
                collisions.above = directionY == 1;
            }
        }
    }
 
    void ClimbSlope(ref Vector3 velocity, float slopeAngle) {
        float moveDistance = Mathf.Abs (velocity.x);
        float climbVelocityY = Mathf.Sin (slopeAngle * Mathf.Deg2Rad) * moveDistance;
 
        if (velocity.y <= climbVelocityY) {
            velocity.y = climbVelocityY;
            velocity.x = Mathf.Cos (slopeAngle * Mathf.Deg2Rad) * moveDistance * Mathf.Sign (velocity.x);
            collisions.below = true;
            collisions.climbingSlope = true;
            collisions.slopeAngle = slopeAngle;
        }
    }
 
    void UpdateRaycastOrigins() {
        Bounds bounds = collider.bounds;
        bounds.Expand (skinWidth * -2);
 
        raycastOrigins.bottomLeft = new Vector2 (bounds.min.x, bounds.min.y);
        raycastOrigins.bottomRight = new Vector2 (bounds.max.x, bounds.min.y);
        raycastOrigins.topLeft = new Vector2 (bounds.min.x, bounds.max.y);
        raycastOrigins.topRight = new Vector2 (bounds.max.x, bounds.max.y);
    }
 
    void CalculateRaySpacing() {
        Bounds bounds = collider.bounds;
        bounds.Expand (skinWidth * -2);
 
        horizontalRayCount = Mathf.Clamp (horizontalRayCount, 2int.MaxValue);
        verticalRayCount = Mathf.Clamp (verticalRayCount, 2int.MaxValue);
 
        horizontalRaySpacing = bounds.size.y / (horizontalRayCount - 1);
        verticalRaySpacing = bounds.size.x / (verticalRayCount - 1);
    }
 
    struct RaycastOrigins {
        public Vector2 topLeft, topRight;
        public Vector2 bottomLeft, bottomRight;
    }
 
    public struct CollisionInfo {
        public bool above, below;
        public bool left, right;
 
        public bool climbingSlope;
        public float slopeAngle, slopeAngleOld;
 
        public void Reset() {
            above = below = false;
            left = right = false;
            climbingSlope = false;
 
            slopeAngleOld = slopeAngle;
            slopeAngle = 0;
        }
    }
 
}
 
cs

145째 줄의 CollisionInfo 구조체에 추가적인 정보가 생겼다.

기존에 상,하,좌,우 4방향으로의 접촉 여부만을 검사하기 위한 정보만 있던것과 달리

이전 프레임에서의 경사각, 현재 경사각, 경사를 오르는지 여부를 알기위한 변수들이 추가되었다.

그럼 추가된 구조체 변수들이 어떤식으로 쓰이는지 확인해보자

 

 

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
    void HorizontalCollisions(ref Vector3 velocity) {
        float directionX = Mathf.Sign (velocity.x);
        float rayLength = Mathf.Abs (velocity.x) + skinWidth;
        
        for (int i = 0; i < horizontalRayCount; i ++) {
            Vector2 rayOrigin = (directionX == -1)?raycastOrigins.bottomLeft:raycastOrigins.bottomRight;
            rayOrigin += Vector2.up * (horizontalRaySpacing * i);
            RaycastHit2D hit = Physics2D.Raycast(rayOrigin, Vector2.right * directionX, rayLength, collisionMask);
 
            Debug.DrawRay(rayOrigin, Vector2.right * directionX * rayLength,Color.red);
 
            if (hit) {
 
                float slopeAngle = Vector2.Angle(hit.normal, Vector2.up);
 
                if (i == 0 && slopeAngle <= maxClimbAngle) {
                    float distanceToSlopeStart = 0;
                    if (slopeAngle != collisions.slopeAngleOld) {
                        distanceToSlopeStart = hit.distance-skinWidth;
                        velocity.x -= distanceToSlopeStart * directionX;
                    }
                    ClimbSlope(ref velocity, slopeAngle);
                    velocity.x += distanceToSlopeStart * directionX;
                }
 
                if (!collisions.climbingSlope || slopeAngle > maxClimbAngle) {
                    velocity.x = (hit.distance - skinWidth) * directionX;
                    rayLength = hit.distance;
 
                    if (collisions.climbingSlope) {
                        velocity.y = Mathf.Tan(collisions.slopeAngle * Mathf.Deg2Rad) * Mathf.Abs(velocity.x);
                    }
 
                    collisions.left = directionX == -1;
                    collisions.right = directionX == 1;
                }
            }
        }
    }
 
    void ClimbSlope(ref Vector3 velocity, float slopeAngle) {
        float moveDistance = Mathf.Abs (velocity.x);
        float climbVelocityY = Mathf.Sin (slopeAngle * Mathf.Deg2Rad) * moveDistance;
 
        if (velocity.y <= climbVelocityY) {
            velocity.y = climbVelocityY;
            velocity.x = Mathf.Cos (slopeAngle * Mathf.Deg2Rad) * moveDistance * Mathf.Sign (velocity.x);
            collisions.below = true;
            collisions.climbingSlope = true;
            collisions.slopeAngle = slopeAngle;
        }
    }
cs

 

 

먼저 HorizontalCollsions를 먼저 살펴본다.

수평방향 ray에서 경사가 감지되었을 경우 14번째 줄에서 감지된 장애물이 얼마나 기울어 젔는지의 각도 정보를 구하고 slopeAngle에 저장한다. 

위그림처럼 hit.normal과 Vector2.up사이의 각도를 통해 경사면의 각도를 얻을 수 있다.

 

16번째 줄에서 i는 0일경우(제일 아래의 ray일 경우) 현재 경사면의 각도 slopeAngle이 캐릭터가 오를 수 있는 경사면의 각도 maxClimbAngle 보다 낮을 경우 경사면을 오르는것을 허용하게된다.

18번째 이제 실질적인 이동에 관여해보자. ray의 감지길이와 해당프레임에서의 이동거리가 동일하다는것을 명심하라

hit.distance를 제일 아래의 ray가 경사면을 감지하는 거리를 얻을 수 있다. 이 값에 skinWidth를 빼면 distanceToSlopeStart를 얻는다. 즉, 캐릭터는 distanceToSlopStart만큼은 무조건 이동이 가능하다는것이다.

 

 

 하지만 이 거리만큼 x축으로 이동하면 velocity.x-distanceToSlopStart만큼의 거리가 남게되는데 이만큼의 거리에 대해서는 빗면을 따라 캐릭터가 상승하게끔 만들어 주어야 한다. 이를 위해 ClimbSlope함수에 velocity.x-distanceToSlopeStart와 SlopeAngle을 함께 전달하여 빗면을 따라 올라가게끔 구현한다.

1
2
3
4
5
6
7
8
9
10
11
12
    void ClimbSlope(ref Vector3 velocity, float slopeAngle) {
        float moveDistance = Mathf.Abs (velocity.x);
        float climbVelocityY = Mathf.Sin (slopeAngle * Mathf.Deg2Rad) * moveDistance;    //회전변환에 의한 y성분 값이다.
 
        if (velocity.y <= climbVelocityY) {
            velocity.y = climbVelocityY;
            velocity.x = Mathf.Cos (slopeAngle * Mathf.Deg2Rad) * moveDistance * Mathf.Sign (velocity.x);    //회전변환에 의한 x성분 값이다.
            collisions.below = true;
            collisions.climbingSlope = true;
            collisions.slopeAngle = slopeAngle;
        }
    }
cs

$x$축으로의 이동속도를 빗면으로 따라가게끔 회전시킨 값으로 바꿔주면된다. 

전체적인 원리는 벡터의 회전을 이용한것이다.

우선 $y$축으로의 이동속도를 구한다.

그리고 빗면에서 점프등에 의해 현재 $y$축 속도가 높은 경우를 제외하고 빗면을 따라가게끔 했을때의 속도보다 낮다면 $y$축 이동속도가 빗면을 따라 이동하도록 회전변환된 값으로 수정해주고 $x$축 속도도 수정해준후 collisionInfo에서 상태를 고처주면된다.

2개의 빨간색 캐릭터가 길이가 같은 초록색 구간을 같은 시간만에 지나간것을 확인 할 수 있다.

 

 

ClimbSlop 호출이 끝나면 distanceToSlopeStart를 다시 더해준다

 

이제 26번째 줄에서 새로운 if문이 시작된다.

이는 오르막길을 올라갈 수 없을경우 진입하는 코드이다.

결론적으로 이 코드는 오르막길을 진입할 수 없게 만들며, 점프를 통해 경사면 한가운데에 도달하더라도 곧바로 바닥으로 미끄러 지게끔 한다.

28번째 줄에서 rayLength를 경사면까지의 거리인 hit.distance로 수정한다.

그리고 이미 경사면 한가운데에 있을 경우 77번째 줄에서 수평 이동속도를 경사각에 맞추어 수직속도로 변환시킨다.

그리고 경사면이 있는 쪽에 장애물이 있다고 표시해준다.

즉, 수평이동시에 경사면이 있다면 수평방향으로의 이동속도를 수직속도로 변환시켜준다.

 

 

이제 VerticalCollision을 살펴보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    void VerticalCollisions(ref Vector3 velocity) {
        float directionY = Mathf.Sign (velocity.y);
        float rayLength = Mathf.Abs (velocity.y) + skinWidth;
 
        for (int i = 0; i < verticalRayCount; i ++) {
            Vector2 rayOrigin = (directionY == -1)?raycastOrigins.bottomLeft:raycastOrigins.topLeft;
            rayOrigin += Vector2.right * (verticalRaySpacing * i + velocity.x);
            RaycastHit2D hit = Physics2D.Raycast(rayOrigin, Vector2.up * directionY, rayLength, collisionMask);
 
            Debug.DrawRay(rayOrigin, Vector2.up * directionY * rayLength,Color.red);
 
            if (hit) {
                velocity.y = (hit.distance - skinWidth) * directionY;
                rayLength = hit.distance;
 
                if (collisions.climbingSlope) {
                    velocity.x = velocity.y / Mathf.Tan(collisions.slopeAngle * Mathf.Deg2Rad) * Mathf.Sign(velocity.x);
                }
 
                collisions.below = directionY == -1;
                collisions.above = directionY == 1;
            }
        }
    }
cs

이전코드와 크게 달라진것은 없다.

다만 16번재 줄에서 경사면에 접촉 했음이 확인되었다면

수직방향으로의 이동속도를 경사면 각도에 맞추어 수평방향으로 변환시킨것이 전부이다.

수직속력을 수평속력으로 바꾸는 이 코드가 무슨 역할을 하는것인지는 잘 모르겠다

테스트 해본 결과 경사면에 서있다고 if(hit) 내부 코드가 호출되는것도 아니었다

 

 

'Unity5 > 기타' 카테고리의 다른 글

3. 2d 플랫포머 - 캐릭터 이동  (0) 2020.07.26
코루틴  (0) 2020.06.06
mechanim(2d)  (0) 2020.06.05
Ray와 Raycast충돌  (0) 2020.06.05
오브젝트 이동하기  (0) 2020.06.05
Comments