Post

[Unity] 이동,공격,대시,잔상 구현2화05.08 ~ 05.11 일지

[Unity] 이동,공격,대시,잔상 구현2화05.08 ~ 05.11 일지

Player Code

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
using UnityEngine;

public class Player : MonoBehaviour
{
    // -- components -- 
    Rigidbody2D rb;
    public float maxSpeed;
    public float animAccel = 5f;
    SpriteRenderer spriteRenderer;
    Animator anim;
    

    public float jumpForce;
    public LayerMask groundMask;
    public Vector2 groundCheckSize = new Vector2(0.45f, 0.05f);
    bool isGrounded;

    //-- Dash related
    public float dashSpeed = 10f;
    public float dashDuration = 0.2f;
    public float dashCooldown = 0.5f;

    float lastDashTime = 0f;
    float dashTime = 0f;
    bool isDashing = false;
    Vector2 dashDirection;

    // -- 잔상
    public GameObject afterImagePrefab;
    public float afterImageInterval = 0.05f;
    float afterImageTimer = 0f;

    
    void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
        spriteRenderer = GetComponent<SpriteRenderer>();
        anim = GetComponent<Animator>();

    }

    void Update()
    {
        // -- Jump
        if (Input.GetKeyDown(KeyCode.Space) && !anim.GetBool("isJumping"))
        {
            rb.AddForce(Vector2.up * jumpForce, ForceMode2D.Impulse);
            anim.SetBool("isJumping", true);
        }
        // -- stop speed
        if (Input.GetButtonUp("Horizontal"))
            rb.linearVelocity = new Vector2(rb.linearVelocity.normalized.x * 0.5f, rb.linearVelocity.y);

        if (Input.GetButtonDown("Horizontal"))
            spriteRenderer.flipX = Input.GetAxisRaw("Horizontal") == -1;

        //-- Idle animator
        if (Mathf.Abs(rb.linearVelocity.x) < 0.3f)
            anim.SetBool("isWalking", false);
        else
            anim.SetBool("isWalking", true);

        if (Input.GetKeyDown(KeyCode.Space) && isGrounded)           // 추가
            rb.linearVelocity = new Vector2(rb.linearVelocity.x, jumpForce);     // 추가

        //-- Dash animator
        if(Input.GetKeyDown(KeyCode.LeftShift) && !isDashing && Time.time > lastDashTime + dashCooldown)
        {
            isDashing = true;
            dashTime = dashDuration;
            lastDashTime = Time.time;

            float direction = Input.GetAxisRaw("Horizontal");
            if(direction == 0)
            {
                direction = spriteRenderer.flipX ? -1 : 1;
            }

            dashDirection = new Vector2(direction, 0).normalized;

            rb.linearVelocity = Vector2.zero; // Reset velocity before dashing
            rb.position += dashDirection * dashSpeed * Time.fixedDeltaTime * 8f; // Apply dash speed

            anim.SetBool("isDashing", true);
        } 

    }

    void FixedUpdate()
    {
        // Dashing
        if (isDashing)
        {
            rb.linearVelocity = dashDirection * dashSpeed;

            //--create after image;
            afterImageTimer -= Time.fixedDeltaTime;
            if(afterImageTimer <= 0f)
            {
                CreateAfterImage();
                afterImageTimer = afterImageInterval;
            }
            dashTime -= Time.fixedDeltaTime;
            // -- dash time
            if (dashTime <= 0)
            {
                isDashing = false;
                anim.SetBool("isDashing", false);
            }
            return;
        }
        // Movement
        float h = Input.GetAxisRaw("Horizontal");
        rb.linearVelocity = new Vector2(h * maxSpeed, rb.linearVelocity.y);
        if (Mathf.Abs(h) > 0.01f)
            spriteRenderer.flipX = h < 0;

        rb.AddForce(Vector2.right * h * 10f, ForceMode2D.Force);

        // Clamp the player's speed
        rb.linearVelocity = new Vector2(Mathf.Clamp(rb.linearVelocity.x, -maxSpeed, maxSpeed), rb.linearVelocity.y);   // �� ����

        float targetSpeed = Mathf.Abs(rb.linearVelocity.x) / maxSpeed;  // 0~1
        targetSpeed = Mathf.Lerp(1f, 2f, targetSpeed);

        anim.speed = Mathf.Lerp(anim.speed, targetSpeed,
                                Time.fixedDeltaTime * animAccel);

        if (rb.linearVelocity.y < 0)
        {
            Debug.DrawRay(transform.position, Vector3.down * 1f, Color.blue);
            // Check if the player is grounded
            RaycastHit2D rH = Physics2D.Raycast(rb.position, Vector3.down, 1, LayerMask.GetMask("Platform"));
            if (rH.collider != null)
            {
                Debug.Log(rH.collider.name);
                anim.SetBool("isJumping", false);
            }
        }

    }

    void CreateAfterImage()
    {
        if (afterImagePrefab == null) return;

        GameObject img = Instantiate(afterImagePrefab, transform.position, Quaternion.identity);
        SpriteRenderer imgSpr = img.GetComponent<SpriteRenderer>();
        SpriteRenderer playerSpr = spriteRenderer;

        if(imgSpr != null && playerSpr != null)
        {
            imgSpr.sprite = playerSpr.sprite;
            imgSpr.flipX = playerSpr.flipX;
            imgSpr.transform.localScale = transform.localScale;
            imgSpr.color = new Color(1f,1f,1f, 0.5f); // 50% alpha
        }
    }

}

Player AtkCode

Atk

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
using UnityEngine;

public class Attack : MonoBehaviour
{
    Animator anim;
    int hashAttackCount = Animator.StringToHash("AtkCount");

    void Start()
    {
        TryGetComponent(out anim);
    }

     void Update()
    {
        if (Input.GetKeyDown(KeyCode.Z))
        {
            AttackCount = (AttackCount + 1) % 3;
            anim.SetTrigger("Attack");

        }
    }

    // Update is called once per frame
    public int AttackCount
    {
        get => anim.GetInteger(hashAttackCount);
        set => anim.SetInteger(hashAttackCount, value);
    }
}

ResetAtk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using UnityEditor.Tilemaps;
using UnityEngine;

public class ResetAtk : StateMachineBehaviour
{

    [SerializeField] string triggerName = "Attack";
    public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        animator.ResetTrigger(triggerName);
    }

}


AfterImagePrefab Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using UnityEngine;

public class AfterImage : MonoBehaviour
{
 

    public float duration = 0.3f;
    void Start()
    {
        Destroy(gameObject, duration);
    }

}


약 3일간, 플레이어의 기본동작을 구현하였다. 생각보다 구현하면서 문제점이 몇가지 있었는데, 아무래도 Player 이미지로만 구성이 되어있고, 애니메이션도 마찬가지다보니, 캐릭터와 무기가 일체형인게 문제였던것 같다. 또, 연속공격의 경우도 아래의 이미지와 같이 만들어진것을 사용하다보니, 문제가 생겼다.

Image

일단 첫번째로 이미지를 부위별로 분해할수 있을까?를 먼저 고민을 했다. 왜냐하면 처음에 구현을 생각했을때, 무기별로 구현을 하면 좋겠다. 라고 생각을 했기 때문이다. 또 연속공격이기때문에, 얘내들을 하나씩 연결 시켜서 ATTACK1이 들어가게되면 → 2 → 3 이런식으로 구현을 할지, 하나의 파일로 구현을 할지도 문제였다.

그치만 하나의 파일로 하게된다면 코드로 애니메이션부분을 나눠주어야하기때문에 번잡스러워서 각 파일을 구현을 하기 시작했다.


구현의 순서

이동 → 점프 → 대시 → 공격

이동

먼저 Player의 이동을 구현을 시도를 했고, 이동은 금방 할수있고, 많은 코드가 배포가 되어있기 때문에, 그쪽 코드를 확인하는게 더 나을수도 있다.

점프

점프는 최대한 로그라이크 느낌을 주기위해서, 엔진 자체의 중력값도 변경해보고, JumpPower의 값을 만들어서 소수점 단위로 넣어서 체크를 해주었다. 일단은 최대한 전투의 시원함을 보여주고싶어서, 점프 대시를 막진 않았으나, 나중에 취업을 하여 구현을 하게된다면, 게임에 따라 구현점이 달라질수도 있겠다 라는 생각이 들었다. 점프의 경우도 애니메이션이 start → transition →end 등으로 구성이 되어있어서, 높이를 어떻게 체크를 해야할까?도 기준이 되었고, 그냥 점프를 2중으로 하진 않으니, 애니메이션을 이어서 구현을 했다.

공격

공격은 애니메이션의 콤보가 있었기 때문에, 콤보를 어떻게 구현하는가? 를 고민을 제일 많이 했던것 같다. (챗지피티에게도 물어봤고, 많은 구현 방법을 찾아봤다.) 대부분의 플머분들은 코루틴 방식을 사용하여 구현을 하였던것 같다. 지피티도 비슷하게 제시를 하였다. 나는 최대한 가볍게 만들고싶은 이유로, 코루틴방식보다 그냥 맨땅의 헤딩을 했던것 같다. 처음 시작은 bool 값으로 각 애니메이션의 끝을 체크해서 끝났어? → true && 0.2초내에 사용자가 한번더 공격을 눌렀니? → true가 나왔을때 애니메이션을 잇도록 하였다. 근데 문제가 생겼던게, 시간초도 체크해주고, 사용자의 z누른 횟수를 체크하다보니까, 공격이 한번만 나가고 atk가 무한 true 버그가 생겼던것이다. 왜지? 라는 생각으로 로그 디버깅을 여러번 했는데, 애니메이션이 끝났니? 부분에서 인식을 아얘하지 못하던 버그가 있었다.(이게 왜 그러지?만 반나절 날림. 이론상 맞는거같은데 ,,) 지피티도 물어보면 코드만 정리해주지, 개선을 해주진 않더라 .. 하여, 공격cs를 여러개로 나눠서 하는것으로 변경하였다. 근데? 코드는 비슷한데 왜 되는지는 모르겠으니, 코드를 좀 보고있었는데 잘 모르겠다.. 그냥 되니까 그대로 쓰고싶다. 뭔가 다른 코드랑 꼬였던것 같은데, Atk index가 올라가지 않아서 좀 곤란했다.

대쉬

대시는 다른게임을 참고를 많이했는데, 코드를 짜다보니 아얘 다른코드가 되어있었다. 이동코드부분에서 추출해와서 LeftShift를 누르게 되면, 순간적인 텔레포트느낌의 이동을 할수 있도록 코드를 짜놨다. 그냥 이동부분에서 이동값에 power를 더 가해서, 캐릭터가 어느지점을 건너뛰게끔 만들어놨다.(실제론 이동임). 그냥 이동만 만들어두니까, 좀 허전하여 prefab을 사용하여 대시를 사용한다면 분신이 생성되도록 만들어놨는데, 이것도 플머가 값을 조정해서 분신(잔상)의 개수를 조절할 수 있도록 만들어놨다.


다음주

다음주엔 일단 Enemy를 구현하여 타격 테스트를 만들것이고 HP,방어력,공격력을 구현할것이다. 또한 지난번에 올렸던 게임 코드를 참고하여, 유니티엔진 안의 NAV기능을 사용하여 몬스터가 player를 일정반경안에 오면 찾아올수 있도록 구현을 해볼 생각이다.

또한 플레이어의 부위별 구분할수 있는 작업이 가능하다면, 무기를 여러개 구현하고, 파티클작업도 하여, 뭔가 무기에서 번쩍번쩍하게 나는 특수효과를 줄 생각이다.

이번 개인프로젝트는 소리를 넣으려고 했는데, 사실 솔직히 모르겠다. 이것저것 하는 프로젝트가 좀 있어서, 찾는 시간이 있다면 넣어보도록 할것같다. 고급알고리즘,코테준비를 쭉하다가 유니티를 건드니까, 하는맛이 좀 있는거같아서 신나서 코딩했다.

고급알고리즘/자료구조/알고리즘,자료구조 도 꾸준히 업로드를 할예정이니, 사실 매주 좀 놓히는게 많아서 좀 아쉽긴한데, 모든걸 다 챙길순 없으니, 내가 코딩할때 즐거운순으로 코드를 짜볼 예정이다.

This post is licensed under CC BY 4.0 by the author.