[Go-Unity 3D] 20. Animations/ Simple Combat โญโญ

์—…๋ฐ์ดํŠธ:

์นดํ…Œ๊ณ ๋ฆฌ:

ํƒœ๊ทธ: , ,


Simple Combat

๊ณ ๋ฐ•์‚ฌ์˜ ์œ ๋‹ˆํ‹ฐ ๊ธฐ์ดˆ๋ฅผ ์ •๋ฆฌ.

1. ๊ฒŒ์ž„ ์›”๋“œ, ์บ๋ฆญํ„ฐ ๊ตฌ์„ฑ

Asset

1.๊ตฌ์กฐ๋ฌผ, 2.์บ๋ฆญํ„ฐ ๋ชจ๋ธ, 3.์บ๋ฆญํ„ฐ ์ „ํˆฌ ์• ๋‹ˆ๋ฉ”์ด์…˜

  1. ๊ตฌ์กฐ๋ฌผ : ์ˆ˜์—…์ž๋ฃŒ
  2. ์บ๋ฆญํ„ฐ ๋ชจ๋ธ : Unity-chan -> Data ๋‹ค์šด๋กœ๋“œ -> ๋™์˜ํ›„ ์‚ฌ์šฉ ->์บ๋ฆญํ„ฐ๋“ค
  3. Asset Store -> RPG Character Mecanim Animation Pack Free ๋‚ด๋ ค ๋ฐ›๊ธฐ
  4. ํ•„์š”์—†๋Š” ํด๋” ์ •๋ฆฌํ•˜์—ฌ ์ตœ์ ํ™”



๊ฒŒ์ž„ ์›”๋“œ ์บ๋ฆญํ„ฐ

  1. ๊ฒŒ์ž„์›”๋“œ ์ƒ์„ฑ
  2. ์นด๋ฉ”๋ผ ๊ฐ๋„ ์กฐ์ •
  3. ์บ๋ฆญํ„ฐ ์„ธ๋ถ€ ์กฐ์ •
    • Rig ํƒญ Optimize Game Object ์ตœ์ ํ™”, ์˜ค๋ฅธ์†์— ๋ฌด๊ธฐ๋“ค๊ธฐ๋•Œ๋ฌธ์— Right hand ์ฒดํฌ Right Hand ์ž์‹์œผ๋กœ ๋ฌด๊ธฐ ์ƒ์„ฑ, ์œ„์น˜, ํšŒ์ „๊ฐ’ ์กฐ์ •, Mesh ์— Materials ์ถ”๊ฐ€
    • ์บ๋ฆญํ„ฐ ์ƒ์„ฑ์‹œ ๋งˆ์  ํƒ€,ํ•‘ํฌ์ƒ‰ ํ•ด๊ฒฐ -> ์‚ฌ์šฉ๋œ Materials Shader : Standard ๋กœ ๋ณ€๊ฒฝ
    • player ์˜ค๋ธŒ์ ํŠธ Character Controller ์„ค์ •



2. Script

CameraController.cs

์นด๋ฉ”๋ผ ์ œ์–ด cs

CameraController.cs

using UnityEngine;

public class CameraController : MonoBehaviour
{
    [SerializeField]
    private Transform   target;             // ์นด๋ฉ”๋ผ๊ฐ€ ์ถ”์ ํ•˜๋Š” ๋Œ€์ƒ
    [SerializeField]
    private float       minDistance = 3;    // ์นด๋ฉ”๋ผ์™€ target์˜ ์ตœ์†Œ ๊ฑฐ๋ฆฌ
    [SerializeField]
    private float       maxDistance = 30;   // ์นด๋ฉ”๋ผ์™€ target์˜ ์ตœ๋Œ€ ๊ฑฐ๋ฆฌ
    [SerializeField]
    private float       wheelSpeed = 500;   // ๋งˆ์šฐ์Šค ํœ  ์Šคํฌ๋กค ์†๋„
    [SerializeField]
    private float       xMoveSpeed = 500;   // ์นด๋ฉ”๋ผ์˜ y์ถ• ํšŒ์ „ ์†๋„
    [SerializeField]
    private float       yMoveSpeed = 250;   // ์นด๋ฉ”๋ผ์˜ x์ถ• ํšŒ์ „ ์†๋„
    private float       yMinLimit = 5;      // ์นด๋ฉ”๋ผ x์ถ• ํšŒ์ „ ์ œํ•œ ์ตœ์†Œ ๊ฐ’
    private float       yMaxLimit = 80;     // ์นด๋ฉ”๋ผ x์ถ• ํšŒ์ „ ์ œํ•œ ์ตœ๋Œ€ ๊ฐ’
    private float       x, y;               // ๋งˆ์šฐ์Šค ์ด๋™ ๋ฐฉํ–ฅ ๊ฐ’
    private float       distance;           // ์นด๋ฉ”๋ผ์™€ target์˜ ๊ฑฐ๋ฆฌ
    
    private void Awake()
    {
        // ์ตœ์ดˆ ์„ค์ •๋œ target๊ณผ ์นด๋ฉ”๋ผ์˜ ์œ„์น˜๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ distance ๊ฐ’ ์ดˆ๊ธฐํ™”
        distance = Vector3.Distance(transform.position, target.position);
        // ์ตœ์ดˆ ์นด๋ฉ”๋ผ์˜ ํšŒ์ „ ๊ฐ’์„ x, y ๋ณ€์ˆ˜์— ์ €์žฅ
        Vector3 angles = transform.eulerAngles;
        x = angles.y;
        y = angles.x;
    }
    
    private void Update() 
    {
        // target์ด ์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด ์‹คํ–‰ ํ•˜์ง€ ์•Š๋Š”๋‹ค
        if ( target == null ) return;

        // ๋งˆ์šฐ์Šค๋ฅผ x, y์ถ• ์›€์ง์ž„ ๋ฐฉํ–ฅ ์ •๋ณด
        x += Input.GetAxis("Mouse X") * xMoveSpeed * Time.deltaTime; 
        y -= Input.GetAxis("Mouse Y") * yMoveSpeed * Time.deltaTime; 
        // ์˜ค๋ธŒ์ ํŠธ์˜ ์œ„/์•„๋ž˜(x์ถ•) ํ•œ๊ณ„ ๋ฒ”์œ„ ์„ค์ • 
        y = ClampAngle(y, yMinLimit, yMaxLimit); 
        // ์นด๋ฉ”๋ผ์˜ ํšŒ์ „(Rotation) ์ •๋ณด ๊ฐฑ์‹ 
        transform.rotation = Quaternion. Euler(y, x, 0);

        // ๋งˆ์šฐ์Šค ํœ  ์Šคํฌ๋กค์„ ์ด์šฉํ•ด target๊ณผ ์นด๋ฉ”๋ผ์˜ ๊ฑฐ๋ฆฌ ๊ฐ’(distance) ์กฐ์ ˆ 
        distance -= Input.GetAxis("Mouse ScrollWheel") * wheelSpeed * Time.deltaTime;
        // ๊ฑฐ๋ฆฌ๋Š” ์ตœ์†Œ, ์ตœ๋Œ€ ๊ฑฐ๋ฆฌ๋ฅผ ์„ค์ •ํ•ด์„œ ๊ทธ ๊ฐ’์„ ๋ฒ—์–ด๋‚˜์ง€ ์•Š๋„๋ก ํ•œ๋‹ค
        distance = Mathf.Clamp(distance, minDistance, maxDistance);
    }

    private void LateUpdate()
    {
        // target์ด ์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด ์‹คํ–‰ ํ•˜์ง€ ์•Š๋Š”๋‹ค
        if (target == null ) return;

        // ์นด๋ฉ”๋ผ์˜ ์œ„์น˜(Position) ์ •๋ณด ๊ฐฑ์‹ 
        // target์˜ ์œ„์น˜๋ฅผ ๊ธฐ์ค€์œผ๋กœ distacne๋งŒํผ ๋–จ์–ด์ ธ์„œ ์ซ“์•„๊ฐ„๋‹ค
        transform.position = transform.rotation * new Vector3(0, 0, -distance) + target.position;
    }
    
    private float ClampAngle(float angle, float min, float max)
    {
        if (angle< -360) angle += 360;
        if (angle > 360) angle -= 360;

        return Mathf.Clamp (angle, min, max);
    }
}


  • Target ์„ ์ค‘์‹ฌ์œผ๋กœ ํšŒ์ „ํ™•๋Œ€ ์นด๋ฉ”๋ผ ๋ชจ์…˜
  • ๋นˆ ์˜ค๋ธŒ์ ํŠธ ๋ฅผ ์บ๋ฆญํ„ฐ ์ค‘์‹ฌ์œ„์น˜์— ์œ„์น˜ํ•˜๊ณ  Target ์œผ๋กœ ์ง€์ •
  • Clamp ์ตœ์†Œ ์ตœ๋Œ€๊ฐ’ ์ง€์ •



Movement3D.cs

Movement3D.cs

using UnityEngine;

public class Movement3D : MonoBehaviour
{
    [SerializeField]
    private float   moveSpeed = 5;      // ์ด๋™ ์†๋„
    [SerializeField]
    private float   gravity = -9.81f;   // ์ค‘๋ ฅ ๊ณ„์ˆ˜
    [SerializeField]
    private float   jumpForce = 3.0f;   // ์ ํ”„ ํž˜
    private Vector3 moveDirection;      // ์ด๋™ ๋ฐฉํ–ฅ

    private CharacterController characterController;

    public float MoveSpeed
    {
        // ์ด๋™์†๋„๋Š” 2~5 ์‚ฌ์ด์˜ ๊ฐ’๋งŒ ์„ค์ • ๊ฐ€๋Šฅ
        set => moveSpeed = Mathf.Clamp (value, 2.0f, 5.0f);
    }
    
    private void Awake()
    {
        characterController = GetComponent<CharacterController>();
    }
    
    private void Update()
    {
        // ์ค‘๋ ฅ ์„ค์ •, ํ”Œ๋ ˆ์ด์–ด๊ฐ€ ๋•…์„ ๋ฐŸ๊ณ  ์žˆ์ง€ ์•Š๋‹ค๋ฉด
        // y์ถ• ์ด๋™๋ฐฉํ–ฅ์— gravity * Time.deltaTime์„ ๋”ํ•ด์ค€๋‹ค.
        if ( characterController.isGrounded == false)
        {
            moveDirection.y += gravity * Time.deltaTime;
        }

        // ์ด๋™ ์„ค์ •. CharacterController์˜ Move() ํ•จ์ˆ˜๋ฅผ ์ด์šฉํ•œ ์ด๋™ 
        characterController. Move(moveDirection * moveSpeed * Time.deltaTime);
    }

    public void MoveTo(Vector3 direction)
    {
        moveDirection = new Vector3(direction.x, moveDirection.y, direction.z);
    }

    public void JumpTo()
    {
        //์บ๋ฆญํ„ฐ๊ฐ€ ๋ฐ”๋‹ฅ์„ ๋ฐŸ๊ณ  ์žˆ์œผ๋ฉด ์ ํ”„
        if (characterController.isGrounded == true)
        {
            moveDirection.y = jumpForce;   
        }
    }

}


  • MoveToํ•จ์ˆ˜๋ฅผ ์ด์šฉํ•ด ์ด๋™๋ฐฉํ–ฅ ์„ค์ •
  • CharacterController์˜ Move()ํ•จ์ˆ˜๋ฅผ ์ด์šฉํ•ด ์ด๋™
  • JumpTo ๋กœ ํ”Œ๋ ˆ์ด์–ด ์ ํ”„



PlayerController.cs

PlayerController.cs

using UnityEngine;

public class PlayerController : MonoBehaviour
{
    [SerializeField] 
    private KeyCode     jumpKeyCode = KeyCode.Space;
    [SerializeField] 
    private Transform   cameraTransform;
    private Movement3D  movement3D;
    private PlayerAnimaor playerAnimaor;
    
    private void Awake()
    {
        Cursor.visible      = false;                   // ๋งˆ์šฐ์Šค ์ปค์„œ๋ฅผ ๋ณด์ด์ง€ ์•Š๊ฒŒ 
        Cursor.lockState    = CursorLockMode.Locked;   // ๋งˆ์šฐ์Šค ์ปค์„œ ์œ„์น˜ ๊ณ ์ •
        
        movement3D = GetComponent<Movement3D>();
        playerAnimaor = GetComponentInChildren<PlayerAnimaor>();
    }
    private void Update()
    {
        // ๋ฐฉํ–ฅํ‚ค๋ฅผ ๋ˆŒ๋Ÿฌ ์ด๋™
        float x = Input.GetAxis("Horizontal");
        float z = Input.GetAxis("Vertical");

        // ์• ๋‹ˆ๋ฉ”์ด์…˜ ํŒŒ๋ผ๋ฏธํ„ฐ ์„ค์ • (horizontal, verticla)
        playerAnimaor.OnMovement(x,z);

        // ์ด๋™ ์†๋„ ์„ค์ • (์•ž์œผ๋กœ ์ด๋™ํ• ๋•Œ๋งŒ 5, ๋‚˜๋จธ์ง€๋Š” 2)
        movement3D.MoveSpeed = z > 0? 5.0f: 2.0f;
        // ์ด๋™ ํ•จ์ˆ˜ ํ˜ธ์ถœ (์นด๋ฉ”๋ผ๊ฐ€ ๋ณด๊ณ ์žˆ๋Š” ๋ฐฉํ–ฅ์„ ๊ธฐ์ค€์œผ๋กœ ๋ฐฉํ–ฅํ‚ค์— ๋”ฐ๋ผ ์ด๋™)
        movement3D.MoveTo(cameraTransform.rotation * new Vector3(x, 0, z));

        // ํšŒ์ „ ์„ค์ • (ํ•ญ์ƒ ์•ž๋งŒ ๋ณด๋„๋ก ์บ๋ฆญํ„ฐ์˜ ํšŒ์ „์€ ์นด๋ฉ”๋ผ์™€ ๊ฐ™์€ ํšŒ์ „ ๊ฐ’์œผ๋กœ ์„ค์ •)
        transform.rotation = Quaternion.Euler(0, cameraTransform.eulerAngles.y,0);

        if (Input.GetKeyDown(jumpKeyCode))
        {
            playerAnimaor.OnJump();
            movement3D.JumpTo();
        }
        // ๋งˆ์šฐ์Šค ์ขŒํด๋ฆญ ์–ดํƒ
        if (Input.GetMouseButtonDown(0))
        {
            playerAnimaor.OnAttack();
        }
        // ์šฐํด๋ฆญ ์—ฐ๊ณ„๊ณต๊ฒฉ
        if (Input.GetMouseButtonDown(1))
        {
            playerAnimaor.OnWeaponAttack();
        }
    }
}


  • ๋งˆ์šฐ์Šค ์ปค์„œ ๊ณ ์ •,
  • ์ด๋™์†๋„ ์„ค์ •
  • ์ŠคํŽ˜์ด์Šคํ‚ค ์ž…๋ ฅ์‹œ ์ ํ”„



3. Animation

์บ๋ฆญํ„ฐ์— Animator Controller ์„ค์ •
apply Root Motion(๊ฒŒ์ž„ ์˜ค๋ธŒ์ ํŠธ์˜ ์œ„์น˜์™€ ํšŒ์ „์„ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์ œ์–ดํ•˜๋„๋ก ํ—ˆ์šฉ) ํ™•์ธ

  • 2D simple Blend Tree
    image

  • Jump ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ถ”๊ฐ€, trigerํƒ€์ž… ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ Movement, Jump ์ƒํƒœ์ „์ด, ์ƒํƒœ์ „์ด has exit Time ์ฒดํฌ ํ™•์ธ image

  • ์ œ๊ณต๋˜๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ด๋ฒคํŠธ ํ•จ์ˆ˜๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์œผ๋ฉด ์—๋Ÿฌ๋ฐœ์ƒํ•จ์œผ๋กœ ์‚ญ์ œํ•ด์ค€๋‹ค.(FootL,FootR) image



PlayerAnimaor.cs

PlayerAnimaor.cs

using UnityEngine;

public class PlayerAnimaor : MonoBehaviour
{
    [SerializeField]
    private GameObject attackCollision;
    private Animator    animator;
    
    private void Awake()
    {
        animator = GetComponent<Animator>();
    }
    
    public void OnMovement (float horizontal, float vertical)
    {
        animator.SetFloat("horizontal", horizontal);
        animator.SetFloat("vertical", vertical);
    }

    public void OnJump()
    {
        animator.SetTrigger("onjump");
    }

    public void OnAttack()
    {
        animator.SetTrigger("onattack");
    }
    public void OnWeaponAttack()
    {
        animator.SetTrigger("onweaponattack");
    }
    public void OnAttackCollision()
    {
        attackCollision.SetActive(true);
    }
}




4. ๊ณต๊ฒฉ ์• ๋‹ˆ๋ฉ”์ด์…˜


์—ฐ๊ณ„๊ณต๊ฒฉ

  • Sub-State Machine(์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๊ทธ๋ฃน ๋‹จ์œ„๋กœ ๋ฌถ์–ด์„œ ์ •๋ฆฌํ•˜๋Š” ๊ธฐ๋Šฅ) ์ƒ์„ฑ
    image

  • ์–ดํƒ 3 - 4 - 2 -1 ์ˆœ์œผ๋กœ
  • ๊ณต๊ฒฉ๋ชจ์…˜์ด ๋๋‚œ ํ›„ ๋‹ค์Œ ๋ชจ์…˜์„ ํ•  ์ˆ˜ ์žˆ๊ฒŒ has exit Time์€ ์ฒดํฌ
  • ๋ชจ๋“  ๊ณต๊ฒฉ์€ ๋„์ค‘ ๋Š๊ธธ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— Base Layer์— ์—ฐ๊ฒฐ
    image
  • ์ƒํƒœ์ „์ด๋ณ„ Exit TIme, Transition duration ํ™•์ธ image

์‹คํ–‰ ํ™”๋ฉด
image




5. Simple Combat System

1.์ขŒ,์šฐํด๋ฆญ -> ๊ณต๊ฒฉ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์žฌ์ƒ
2.๊ณต๊ฒฉ ์• ๋‹ˆ๋ฉ”์ด์…˜์˜ ํŠน์ • ํ”„๋ ˆ์ž„์— ์ด๋ฒคํŠธ ํ•จ์ˆ˜ ํ˜ธ์ถœ -> ์ถฉ๋Œ ๋ฐ•์Šค AttackCollision ์˜ค๋ธŒ์ ํŠธ ํ™œ์„ฑํ™”
3.๊ณต๊ฒฉ ์ถฉ๋Œ ๋ฐ•์Šค์— ์˜ค๋ธŒ์ ํŠธ๊ฐ€ ๋ถ€๋”ชํžˆ๋ฉด -> Take Damage() ํ•จ์ˆ˜ ํ˜ธ์ถœ (ํ”ผ๊ฒฉ ์• ๋‹ˆ๋ฉ”์ด์…˜, ์˜ค๋ธŒ์ ํŠธ ์ƒ‰์ƒ๋ณ€๊ฒฝ)


Enemy

1.AnimationController ์ƒ์„ฑ
2.์ถฉ๋Œ Capsule Collider ๋ฒ”์œ„ ์ง€์ •, Meterial ์ง€์ •

๊ณต๊ฒฉ๋ฒ”์œ„

  • ํ”Œ๋ ˆ์ด์–ด์˜ ๊ณต๊ฒฉ ๋ฒ•์œ„๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ•์Šค๋ฅผ ์ƒ์„ฑ , ์œ„์น˜,ํฌ๊ธฐ ์„ค์ •
  • ๋ชจ์Šต์•ˆ๋ณด์ด๊ฒŒ Mesh ์ฒดํฌํ•ด์ œ, collier ํŠธ๋ฆฌ๊ฑฐ ์ฒดํฌ, rigid ์ค‘๋ ฅx
    HITBox



EnemyController.cs

EnemyController.cs

using System.Collections;
using UnityEngine;

public class EnemyController : MonoBehaviour
{
    private Animator            animator;
    private SkinnedMeshRenderer meshRenderer;
    private Color               originColor;

    private void Awake()
    {
        animator     = GetComponent<Animator>();
        meshRenderer = GetComponentInChildren<SkinnedMeshRenderer>();
        originColor  = meshRenderer.material.color;
    }

    public void TakeDamage(int damage)
    {
        // ์ฒด๋ ฅ์ด ๊ฐ์†Œ๋˜๊ฑฐ๋‚˜ ํ”ผ๊ฒฉ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์žฌ์ƒ๋˜๋Š” ๋“ฑ์˜ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑ 
        Debug.Log(damage+"์˜ ์ฒด๋ ฅ์ด ๊ฐ์†Œํ•ฉ๋‹ˆ๋‹ค");
        // ํ”ผ๊ฒฉ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์žฌ์ƒ
        animator.SetTrigger("onhit");
        // ์ƒ‰์ƒ ๋ณ€๊ฒฝ
        StartCoroutine("OnHitColor");
    }

    private IEnumerator OnHitColor()
    {
        // ์ƒ‰์„ ๋นจ๊ฐ„์ƒ‰์œผ๋กœ ๋ณ€๊ฒฝํ•œ ํ›„ 0.1์ดˆ ํ›„์— ์›๋ž˜ ์ƒ‰์ƒ์œผ๋กœ ๋ณ€๊ฒฝ
        meshRenderer.material.color = Color.red;
        yield return new WaitForSeconds (0.1f);
        meshRenderer.material.color = originColor;
    }
}


  • Enemy ํ”ผ๊ฒฉ



PlayerAttackCollision.cs

์ถฉ๋Œ๋ฐ•์Šค, ๋ฐ๋ฏธ์ง€

PlayerAttackCollision.cs

using System.Collections;
using UnityEngine;

public class PlayerAttackCollision : MonoBehaviour
{
    private void OnEnable()
    {
        StartCoroutine("AutoDisable");
    }
    private void OnTriggerEnter (Collider other)
    {
        // ํ”Œ๋ ˆ์ด์–ด๊ฐ€ ํƒ€๊ฒฉํ•˜๋Š” ๋Œ€์ƒ์˜ ํƒœ๊ทธ, ์ปดํฌ๋„ŒํŠธ, ํ•จ์ˆ˜๋Š” ๋ฐ”๋€” ์ˆ˜ ์žˆ๋‹ค
        if (other.CompareTag("Enemy"))
        {
            other.GetComponent<EnemyController>().TakeDamage(10);
        }
    }
    private IEnumerator AutoDisable()
    {
        // 0.1์ดˆ ํ›„์— ์˜ค๋ธŒ์ ํŠธ๊ฐ€ ์‚ฌ๋ผ์ง€๋„๋ก ํ•œ๋‹ค
        yield return new WaitForSeconds(0.1f);
        gameObject.SetActive(false);
    }
}


  • ์ถฉ๋Œ ํƒœ๊ทธ๊ฐ€ enemy ์ด๋ฉด TakeDamage

์• ๋‹ˆ๋ฉ”์ด์…˜์— ์ด๋ฒคํŠธ ํ•จ์ˆ˜ ์ƒ์„ฑ image




6. ์ •๋ฆฌ ์‹คํ–‰ ํ™”๋ฉด

image


์ฐธ๊ณ  : ์œ ๋‹ˆํ‹ฐ TOP


๐Ÿ“”

๋Œ“๊ธ€๋‚จ๊ธฐ๊ธฐ