몬스터애니메이션 컨트롤러 초기

몬스터 추적범위와 스태이트 변경

몬스터애니메이션컨트롤러 Hit

몬스터 추적 후 어택

몬스터 피격 애니메이션

몬스터 피격 시 출혈

플레이어 사망시 몬스터 춤

적 사망시 죽는 애니메이션

6(1).적 사망시 죽는 애니메이션.mp4
3.94MB

 

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEditor.Experimental.GraphView;
using UnityEngine;

public class PlayerCtrl : MonoBehaviour
{
    private Transform tr;
    public float moveSpeed = 10.0f;
    public float turnSpeed = 80.0f;
    private Animation anim;
    private readonly float initHp = 100f;
    public float currentHp;

    public delegate void PlayerDieHandler(); //대리자 정의

    public static event PlayerDieHandler OnPlayerDie; //이벤트 변수 정의

    private void Awake()
    {
        //제일먼저 호출되는 함수
        //스크리브가 비활성화되있어도 호출되는 함수

    }

    private void OnEnable()
    {
        //두번째로 호출되는 함수
        //스크립트가 활성화 될때마다 호출되는 함수
    }

    IEnumerator Start()
    {
        this.currentHp = this.initHp;
        // 세번째로 호출되는 함수 
        //Update함수가 호출되기 전에 호출되는 함수 
        //코루틴으로 호출될수 있는 함수 
        //예) 
        //IEnumerator Start(){}

        tr = GetComponent<Transform>();
        this.anim = GetComponent<Animation>();


        //이름으로 실행 하거나 
        this.anim.Play("Idle");


        //이름으로 가져와서 플레이 하거나 
        //this.anim.clip = this.anim.GetClip("Idle");
        //this.anim.Play();

        this.turnSpeed = 0f;

        yield return new WaitForSeconds(0.3f);

        this.turnSpeed = 80f;

    }

    private void FixedUpdate()
    {
        //일정간격으로 호출되는 함수 (기본값은 0.02초)
        //물리엔진의 계산 주기와 일치

    }

    void Update()
    {
        //프레임마다 호출되는 함수
        //호출 간격이 불규칙하다
        //화면의 렌더링 주기와 일치 함
        float h = Input.GetAxis("Horizontal"); // -1.0 ~ 0 ~ 1
        float v = Input.GetAxis("Vertical"); // -1.0 ~ 0 ~ 1
        float r = Input.GetAxis("Mouse X");

        //Debug.Log("h=" + h);
        //Debug.Log("v=" + v);
        Debug.Log("r=" + r);

        //this.tr.Translate(Vector3.forward * 1 * 0.01f , Space.Self); //로컬좌표 기준으로 이동
        //this.tr.Translate(Vector3.forward * 1 * Time.deltaTime, Space.World); //월드좌표 기준으로 이동

        //매 프레임마다 1유닛씩 이동 => 0.02초
        //this.tr.Translate(Vector3.forward * 1 * Time.deltaTime);


        //매 초마다 1유닛씩 이동
        //프레임 레이트가 서로다른 기기에서도 개발자가 지정한 일정한 속도로 이동하기 위함
        //this.tr.Translate(Vector3.forward * v * Time.deltaTime * moveSpeed); //방향 * 속도 * 시간 => forward = (0,0,1) // v => -1~ 1 사이의 값 => v를 사용해서 앞뿐만이 아닌 뒤로도 이동가능

        //전후좌우 이동방향 벡터 계산
        Vector3 moveDir = (Vector3.forward * v) + (Vector3.right * h); // right = (1,0,0)

        //방향 * 속도 * 시간 
        this.tr.Translate(moveDir.normalized * moveSpeed * Time.deltaTime);

        //Vector3.up 축으로 turnSpeed만큼
        this.tr.Rotate(Vector3.up * r * this.turnSpeed);

        //캐릭터 애니메이션 실행
        PlayerAnim(h, v);


    }

    private void LateUpdate()
    {
        //Update함수가 종료된 후 호출 되는 함수
    }

    private void PlayerAnim(float h, float v)
    {
        //키보드 입력값을 기준으로 동작할 애니메이션 실행
        if (v >= 0.1f)
        {
            //0.25f초간 이전 애니메이션과 실행할 애니메이션을 보간 
            this.anim.CrossFade("RunF", 0.25f); //전진 애니메이션 실행 
        }
        else if (v <= -0.1f)
        {
            this.anim.CrossFade("RunB", 0.25f);//후진 
        }
        else if (h >= 0.1f)
        {
            this.anim.CrossFade("RunR", 0.25f);//오른쪽 
        }
        else if (h <= -0.1f)
        {
            this.anim.CrossFade("RunL", 0.25f);//왼쪽 
        }
        else
        {
            this.anim.CrossFade("Idle", 0.25f);//기본 
        }

    }


    private void OnTriggerEnter(Collider other)
    {
        if(this.currentHp >= 0f && other.CompareTag("Punch"))
        {
            this.currentHp -= 10f;
            Debug.Log($"Player hp = {currentHp / initHp}");

            if (this.currentHp <= 0f)
            {
                this.PlayerDie();
            }

        }
    }
    private void PlayerDie()
    {
        Debug.Log("Player Die!");

        ////Monster 태그를 가진 오브젝트들을 검색
        //GameObject[] monsters = GameObject.FindGameObjectsWithTag("Monster");

        ////모든 몬스터의 OnplayerDie함수실행

        //foreach(GameObject monster in monsters)
        //{
        //    monster.SendMessage("OnPlayerDie", SendMessageOptions.RequireReceiver);
        //}
        PlayerCtrl.OnPlayerDie(); //ㄷ대리차 호출(이벤트 발생 Dispatch)

    }


}

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BulletCtrl : MonoBehaviour
{
    public float damage = 20.0f;
    public float force = 1500f;
    private Rigidbody rb;

    void Start()
    {
        this.rb = this.GetComponent<Rigidbody>();
        this.rb.AddForce(this.transform.forward * force);

        Destroy(gameObject, 3f);
    }

}

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class FollowCam : MonoBehaviour
{
    public Transform targetTr;
    private Transform camTr;

    //대상으로 부터 떨어질 거리 
    [Range(2.0f, 20.0f)]
    public float distance = 10.0f;

    //Y축으로 이동할 높이 
    [Range(0.0f, 10.0f)]
    public float height = 2.0f;

    //반응속도 
    public float damping = 10f;

    //SmoothDamp에 사용할 속도 변수
    private Vector3 velocity = Vector3.zero;

    //카메라 LookAt OffSet
    public float targetOffset = 2.0f;


    void Start()
    {
        this.camTr = GetComponent<Transform>();     //Transform 객체 캐싱 
    }

    private void LateUpdate()
    {
        //추적할 대상의 뒤쪽 distance만큼 이동 
        //높이를 height만큼 이동 
        Vector3 pos = this.targetTr.position +
            (-targetTr.forward * distance) +
            (Vector3.up * height);

        //구면 선형 보간 함수를 사용해 부드럽게 위치를 변경 
        //시작위치, 목표 위치, 시간 
        //this.camTr.position = Vector3.Slerp(this.camTr.position, pos, Time.deltaTime * damping);

        //시작위치, 목표위치, 현재속도, 목표 위치까지 도달할 시간
        this.camTr.position = Vector3.SmoothDamp(this.camTr.position, pos, ref velocity, damping);


        //Camera를 피벗 좌표를 향해 회전 
        this.camTr.LookAt(this.targetTr.position + (targetTr.up * targetOffset));

    }
}

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class FireCtrl : MonoBehaviour
{
    public GameObject bulletPrefab;
    public Transform firePoint;
    public AudioClip fireSfx;
    private AudioSource audioSource;

    private MeshRenderer muzzleFlash;
    private Coroutine muzzleFlashCoroutine;

    private void Start()
    {
        audioSource = GetComponent<AudioSource>();
        muzzleFlash = this.firePoint.GetComponentInChildren<MeshRenderer>();
        muzzleFlash.enabled = false;
    }

    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            this.Fire();
        }
    }

    private void Fire()
    {
        Instantiate(bulletPrefab, firePoint.position, firePoint.rotation);

        audioSource.PlayOneShot(fireSfx, 1f);

        if (muzzleFlashCoroutine != null) StopCoroutine(muzzleFlashCoroutine);

        this.muzzleFlashCoroutine = StartCoroutine(this.ShowMuzzleFlash());
    }

    IEnumerator ShowMuzzleFlash()
    {
        Vector2 offset = new Vector2(Random.Range(0, 2), Random.Range(0, 2)) * 0.5f;
        muzzleFlash.material.mainTextureOffset = offset;



        float angle = Random.Range(0, 360);
        muzzleFlash.transform.localRotation = Quaternion.Euler(0, 0, angle);


        float scale = Random.Range(1, 2);
        muzzleFlash.transform.localScale = Vector3.one * scale;


        muzzleFlash.enabled = true; //보여주기 

        yield return new WaitForSeconds(0.2f);  //0.2초후 

        muzzleFlash.enabled = false;    //안보여주기 
    }
}

 

using UnityEngine;
using System.Collections;
using UnityEditor;


public enum ArrowType
{
    Default,
    Thin,
    Double,
    Triple,
    Solid,
    Fat,
    ThreeD,
}

public static class DrawArrow
{
    public static void ForGizmo(Vector3 pos, Vector3 direction, Color? color = null, bool doubled = false, float arrowHeadLength = 0.2f, float arrowHeadAngle = 20.0f)
    {
        Gizmos.color = color ?? Color.white;

        //arrow shaft
        Gizmos.DrawRay(pos, direction);

        if (direction != Vector3.zero)
        {
            //arrow head
            Vector3 right = Quaternion.LookRotation(direction) * Quaternion.Euler(0, 180 + arrowHeadAngle, 0) * new Vector3(0, 0, 1);
            Vector3 left = Quaternion.LookRotation(direction) * Quaternion.Euler(0, 180 - arrowHeadAngle, 0) * new Vector3(0, 0, 1);
            Gizmos.DrawRay(pos + direction, right * arrowHeadLength);
            Gizmos.DrawRay(pos + direction, left * arrowHeadLength);
        }
    }

    public static void ForDebug(Vector3 pos, Vector3 direction, float duration = 0.5f, Color? color = null, ArrowType type = ArrowType.Default, float arrowHeadLength = 0.2f, float arrowHeadAngle = 30.0f, bool sceneCamFollows = false)
    {
        Color actualColor = color ?? Color.white;
        duration = duration / Time.timeScale;

        float width = 0.01f;

        Vector3 directlyRight = Vector3.zero;
        Vector3 directlyLeft = Vector3.zero;
        Vector3 directlyBack = Vector3.zero;
        Vector3 headRight = Vector3.zero;
        Vector3 headLeft = Vector3.zero;

        if (direction != Vector3.zero)
        {
            directlyRight = Quaternion.LookRotation(direction) * Quaternion.Euler(0, 180 + 90, 0) * new Vector3(0, 0, 1);
            directlyLeft = Quaternion.LookRotation(direction) * Quaternion.Euler(0, 180 - 90, 0) * new Vector3(0, 0, 1);
            directlyBack = Quaternion.LookRotation(direction) * Quaternion.Euler(0, 180, 0) * new Vector3(0, 0, 1);
            headRight = Quaternion.LookRotation(direction) * Quaternion.Euler(0, 180 + arrowHeadAngle, 0) * new Vector3(0, 0, 1);
            headLeft = Quaternion.LookRotation(direction) * Quaternion.Euler(0, 180 - arrowHeadAngle, 0) * new Vector3(0, 0, 1);
        }

        //draw arrow head
        Debug.DrawRay(pos + direction, headRight * arrowHeadLength, actualColor, duration);
        Debug.DrawRay(pos + direction, headLeft * arrowHeadLength, actualColor, duration);

        switch (type)
        {
            case ArrowType.Default:
                Debug.DrawRay(pos, direction, actualColor, duration); //draw center line
                break;
            case ArrowType.Double:
                Debug.DrawRay(pos + directlyRight * width, direction * (1 - width), actualColor, duration); //draw line slightly to right
                Debug.DrawRay(pos + directlyLeft * width, direction * (1 - width), actualColor, duration); //draw line slightly to left

                //draw second arrow head
                Debug.DrawRay(pos + directlyBack * width + direction, headRight * arrowHeadLength, actualColor, duration);
                Debug.DrawRay(pos + directlyBack * width + direction, headLeft * arrowHeadLength, actualColor, duration);

                break;
            case ArrowType.Triple:
                Debug.DrawRay(pos, direction, actualColor, duration); //draw center line
                Debug.DrawRay(pos + directlyRight * width, direction * (1 - width), actualColor, duration); //draw line slightly to right
                Debug.DrawRay(pos + directlyLeft * width, direction * (1 - width), actualColor, duration); //draw line slightly to left
                break;
            case ArrowType.Fat:
                break;
            case ArrowType.Solid:
                int increments = 20;
                for (int i = 0; i < increments; i++)
                {
                    float displacement = Mathf.Lerp(-width, +width, i / (float)increments);
                    //draw arrow body
                    Debug.DrawRay(pos + directlyRight * displacement, direction, actualColor, duration); //draw line slightly to right
                    Debug.DrawRay(pos + directlyLeft * displacement, direction, actualColor, duration); //draw line slightly to left
                                                                                                        //draw arrow head
                    Debug.DrawRay((pos + direction) + directlyRight * displacement, headRight * arrowHeadLength, actualColor, duration);
                    Debug.DrawRay((pos + direction) + directlyRight * displacement, headLeft * arrowHeadLength, actualColor, duration);
                }
                break;
            case ArrowType.Thin:
                Debug.DrawRay(pos, direction, actualColor, duration); //draw center line
                break;
            case ArrowType.ThreeD:
                break;
        }

        /*#if UNITY_EDITOR
            //snap the Scene view camera to a spot where it is looking directly at this arrow.
                if (sceneCamFollows)
                    SceneViewCameraFollower.activateAt(pos + direction, duration, "_arrow");
        #endif*/
    }

    public static void randomStar(Vector3 center, Color color)
    {
        //special: refuse to draw at 0,0.
        if (center == Vector3.zero) return;
        for (int i = 0; i < 2; i++)
            DrawArrow.ForGizmo(center, UnityEngine.Random.onUnitSphere * 1, color, false, 0.1f, 30.0f);
    }

    public static void comparePositions(Transform t1, Transform t2)
    {
        //direct from one to the other:
        ForDebug(t1.position, t2.position - t1.position);

        //direction
        //Vector3 moveDirection = (t2.position-t1.position).normalized;
    }
}

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RemoveBullet : MonoBehaviour
{
    public GameObject sparkEffectPrefab;

    private void OnCollisionEnter(Collision collision)
    {
        if (collision.collider.CompareTag("Bullet"))
        {
            //첫번째 충돌 지점의 정보 추출 
            ContactPoint contactPoint = collision.contacts[0];

            DrawArrow.ForDebug(contactPoint.point, -contactPoint.normal, 10, Color.red);

            //충돌한 총알의 법선 벡터를 쿼터니언으로 변환 
            Quaternion rot = Quaternion.LookRotation(-contactPoint.normal);

            Instantiate(this.sparkEffectPrefab, contactPoint.point, rot);

            Destroy(collision.gameObject);
        }
    }
}

 

using UnityEngine;

public class BarrelCtrl : MonoBehaviour
{
    private int hitCount = 0;
    private Transform trans;
    private Rigidbody rb;
    public GameObject explosionPrefab;
    public Texture[] textures;
    private new MeshRenderer renderer;

    //폭발 반경 
    public float radius = 10f;


    void Start()
    {
        this.trans = this.GetComponent<Transform>();
        this.rb = this.GetComponent<Rigidbody>();

        //자식에 있는 랜더러 컴포넌트를 찾아옴 
        this.renderer = this.GetComponentInChildren<MeshRenderer>();
        //난수 발생 
        int index = Random.Range(0, this.textures.Length);
        //텍스쳐 변경 
        this.renderer.material.mainTexture = textures[index];
    }

    private void OnCollisionEnter(Collision collision)
    {
        if (collision.collider.CompareTag("Bullet"))
        {
            if (++hitCount == 3)
            {
                ExplodeBarrel();
            }
        }
    }

    private void ExplodeBarrel()
    {
        //폭파 이펙트를 생성 
        GameObject exp = Instantiate(explosionPrefab, this.trans.position, Quaternion.identity);
        //이펙트 5초 있다가 제거 
        Destroy(exp, 5.0f);

        //힘을 위로 가함 
        //this.rb.mass = 1.0f;    //질량을 줄여주고 
        //this.rb.AddForce(Vector3.up * 1500);    //위로 힘을 가한다 

        //간접 폭발력 전달 
        IndirectDamage(this.trans.position);

        //3초후 드럼통 제거 
        Destroy(this.gameObject, 3);
    }

    private void IndirectDamage(Vector3 pos)
    {
        //주변에 있는 드럼통들을 모두 추출 
        Collider[] colls = Physics.OverlapSphere(pos, this.radius, 1 << 3);

        foreach (var coll in colls)
        {
            var rb = coll.GetComponent<Rigidbody>();
            rb.mass = 1.0f;
            rb.constraints = RigidbodyConstraints.None;
            rb.AddExplosionForce(1500, pos, radius, 1200);
        }
    }

    private void OnDrawGizmos()
    {
        Gizmos.color = Color.red;
        Gizmos.DrawWireSphere(this.transform.position, this.radius);
    }


}

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class MonsterCtrl : MonoBehaviour
{
    public enum State
    {
        IDLE, TRACE, ATTACK, DIE //ctrl+shift+u : 대문자로 변경
    }

    public State state = State.IDLE;
    public float traceDist = 10f; //추적 사정거리
    public float attackDist = 2.0f;//공격 사거리
    public bool isDie = false;//몬스터 사망여부
    private int hp = 100; //몬스터 체력
    
    private NavMeshAgent agent;
    private Transform playerTrans;
    private Animator anim;
    private GameObject bloodEffectPrefab;

    private readonly int hashTrace = Animator.StringToHash("IsTrace");
    private readonly int hashAttack = Animator.StringToHash("IsAttack");
    private readonly int hashHit = Animator.StringToHash("Hit");
    private readonly int hashPlayerDie = Animator.StringToHash("PlayerDie");
    private readonly int hashSpeed = Animator.StringToHash("Speed");
    private readonly int hashDie = Animator.StringToHash("Die");


    private void OnEnable()
    {
        PlayerCtrl.OnPlayerDie += this.OnPlayerDie;
    }



    void Start()
    {
        //플레이어를 찾아아서 이동한다.
        this.agent = GetComponent<NavMeshAgent>();

        //프리팹을 로드한다 
        this.bloodEffectPrefab = Resources.Load<GameObject>("BloodSprayEffect");

        this.playerTrans = GameObject.FindWithTag("Player").transform;

        this.anim = GetComponent<Animator>();

        

        //이동한다
        this.agent.SetDestination(this.playerTrans.position);

        //몬스터의 상태를 체크 하는 코루틴
        this.StartCoroutine(CheckMonsterState());

        //상태에 따라 몬스터의 행동을 수행하는 코루틴
        this.StartCoroutine(this.MonsterAction());
    }

    IEnumerator MonsterAction()
    {
        while(!this.isDie)
        {
            switch (this.state)
            {
                case State.IDLE:
                    agent.isStopped = true; //추적정지
                    this.anim.SetBool(hashTrace, false);
                    break;
                case State.TRACE:
                    agent.SetDestination(this.playerTrans.position);
                    agent.isStopped = false; //추적을 재개
                    this.anim.SetBool(hashTrace, true);
                    this.anim.SetBool(hashAttack, false);
                    break;
                case State.ATTACK:
                    this.anim.SetBool(hashAttack, true);
                    break;
                case State.DIE:
                    this.isDie = true;
                    agent.isStopped = true;
                    anim.SetTrigger(hashDie);
                    GetComponent<CapsuleCollider>().enabled = false;
                    break;
            
            }


            yield return new WaitForSeconds(0.3f);
        }
    }

    IEnumerator CheckMonsterState()
    {
        while(!this.isDie)
        {
            yield return new WaitForSeconds(0.3f);

            //몬스터 상태가 죽었을 때 코루틴을 즉시 종료
            if(state == State.DIE)
            {
                yield break; //코루틴을 즉시 종료
            }

            //거리 측정
            float distance = Vector3.Distance(this.transform.position, playerTrans.position);

            //공격 사거리 측정
            if(distance <= attackDist)
            {
                state = State.ATTACK;
            }
            else if(distance <= traceDist) //시야에 들어오면
            {
                state = State.TRACE;
            }
            else //공격 사거리에도 안들어오고 시야에도 없다
            {
                state = State.IDLE;
            }
        }
    }

    private void OnCollisionEnter(Collision collision)
    {
        if (collision.collider.CompareTag("Bullet"))
        {
            Destroy(collision.gameObject);
            anim.SetTrigger(this.hashHit);

            //충돌 지점을 가져온다.
            ContactPoint cp = collision.GetContact(0);
            Vector3 pos = cp.point;

            //충돌지점의 법선벡터(Noraml Vector)의 반대 방향으로 가져온다.
            //피격시 피가 튀기는 방향을 조절한다.
            Quaternion rot = Quaternion.LookRotation(-cp.normal);

            //이펙트를 생성
            this.ShowBloodEffect(pos, rot);

            hp -= 10;
            if(hp <= 0)
            {
                state = State.DIE;
            }
        }
    }


    void ShowBloodEffect(Vector3 pos, Quaternion rot)
    {
        //프리팹의 인스턴스화 게임인스턴스, 위치, 회전, 부모
        GameObject blood = Instantiate(this.bloodEffectPrefab, pos, rot, this.transform);

        Destroy(blood, 1.0f);
    }

    private void OnDrawGizmos()
    {
        if(state == State.TRACE)
        {
            Gizmos.color = Color.blue;
            Gizmos.DrawWireSphere(transform.position, traceDist);
        }
        if (state == State.ATTACK)
        {
            Gizmos.color = Color.red;
            Gizmos.DrawWireSphere(transform.position, attackDist);
        }
    }

    public void OnPlayerDie()
    {
        this.StopAllCoroutines();

        agent.isStopped = true;

        anim.SetFloat(hashSpeed, Random.Range(0.8f, 1.2f));

        anim.SetTrigger(hashPlayerDie);
    }

    private void OnDisable()
    {
        //이벤트 리스너를 제거
        PlayerCtrl.OnPlayerDie -= this.OnPlayerDie;
    }


}

'산대특 > 게임 클라이언트 프로그래밍' 카테고리의 다른 글

Occlusion Culling  (0) 2024.03.18
Sprite Shooter - Destroy Barrel and Layer  (0) 2024.03.13
Zombie - IK  (0) 2024.03.07
Zombie - Line Render  (0) 2024.03.07
Tank - Rigidbody.MovePosition  (0) 2024.03.06

 

 

 

 

스프라이트 게임을 자르기 위해 설정들을 이렇게 바꾸어 주었다.

그 후 Sprite Editor를 누르면 여러개의 이미지들이 묶여있는 것을 분할 할 수 있는데

크기로 24*24 여백을 1로 잘랐다.

 

우선 화면의 비율을 선택해주자

필자는 aspect로 하였고 비율을 입력하여 2가지 해상도를 만들었다.

 

비행기가 움직일 수 있게 코드 작성

transform은 time.deltatime이랑 세트로 생각하자!

현재의 위치에 키보드로 받을 위치를 더해준다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour
{
    public float speed;
    void Update()
    {
        float h = Input.GetAxisRaw("Horizontal");
        float v = Input.GetAxisRaw("Vertical");
        Vector3 curPos = this.transform.position;
        Vector3 nextPos = new Vector3(h, v, 0) * speed * Time.deltaTime;
        transform.position = curPos + nextPos;

    }
}

 

움직여보면 원하지 않는 배경 밖으로 나가게 된다.

 

비행기에 Box Collider를 추가하여 부착해주면 초록색 경계가 생긴다.

 

피격 범위로도 사용할 것이므로 콜라이더의 크기를 줄여주었다.

맵의 바깥 부분에 Border라는 빈 오브젝트안에

방향 별로 box collider2D를 붙인 후 범위를 잡아주었다.

 

 

Player :

물리 연산이기에 모두 리지드바디2d적용

물리엔진을 많이 사용하지 않기에 키네틱을 사용

키넥틱이란? 다른 것이랑 충돌이 일어났을 때 영향을 받지 않겠다.

 

보더들은 고정이기때문에 static 사용

 

경계들을 각각 인지를 해야하기에 변수 선언

물리적인 힘은 넣을 필요 없으므로 collider대신에 온트리거 사용하였고,

각 경계마다 tag로 Border를 달아줄 것이다.

물체가 닿은 상태에서 h는 좌우 이동이므로 => 이동시 닿을 떄를 처리하는데 이때 각각 0으로 만들어서 이동을 멈춘다.

물체가 닿지 않았을 때 이동해야하므로 그 경우도 처리해야 한다.

태그를 달아준다.

 

현재 까지의 코드

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour
{
    public float speed;
    public bool isTouchTop;
    public bool isTouchBottom;
    public bool isTouchRight;
    public bool isTouchLeft;

    void Update()
    {
        float h = Input.GetAxisRaw("Horizontal");
        if ((isTouchRight && h == 1) || (isTouchLeft && h == -1))
            h = 0;
        float v = Input.GetAxisRaw("Vertical");
        if ((isTouchTop && v == 1) || (isTouchBottom && v == -1))
            v = 0;
        Vector3 curPos = this.transform.position;
        Vector3 nextPos = new Vector3(h, v, 0) * speed * Time.deltaTime;
        transform.position = curPos + nextPos;
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if(collision.gameObject.tag == "Border")
        {
            switch(collision.gameObject.name)
            {
                case "Top":
                    isTouchTop = true;
                    break;
                case "Bottom":
                    isTouchBottom = true;
                    break;
                case "Right":
                    isTouchRight = true;
                    break;
                case "Left":
                    isTouchLeft = true;
                    break;
            }
        }
    }

    private void OnTriggerExit2D(Collider2D collision)
    {
        switch (collision.gameObject.name)
        {
            case "Top":
                isTouchTop = false;
                break;
            case "Bottom":
                isTouchBottom = false;
                break;
            case "Right":
                isTouchRight = false;
                break;
            case "Left":
                isTouchLeft = false;
                break;
        }
    }
}

 

 

이제 애니메이션을 넣어줄 것이다.

잘라놓은 애니메이션 각각 4개를 클릭해서

하이어라이키에 있는 Player에 드래그해서 넣어주고 애니메이션 폴더를 생성후

각각 Center, Left, Right로 이름을 만들어서 저

플레이어에 애니메이션 컴포넌트가 생성됨

그 후 플레이어(애니메이션 컨트롤러)를 눌러서 이 창을 띄어주고

Make Transition으로 서로의 관계를 만들어 준다.

 

트랜지션은 파라미터로 이동한다.

매개변수 Input.GetAxisRaw() 값 그대로 사

각각의 트랜지션 설정을 해줄것인다.

즉각 반응을 위해 Has Exit Time은 꺼야한다.

Transition Duration은 보통3D에서 모션을 부드럽게 변화시키기 위해 사용하는데

필요 없으므로 0으로 변경

 

이제 인풋 파라미터를 받을 코드를 작성할 것이다.

그전에 애니메이터 변수 생성 후 초기화 진행

 

업데이트 문 안에 작성하고 좌우 방향은 h이므로 h를 넣고 float형이므로

(int)로 강제 형변환

\

배경을 집어 넣을 것인데 넣으면 플레이어가 보이지 않을 수 있다.

그러면 플레이어를 누르고 Order In Layer를 0에서 1로 변경

 

그러면 플레이어가 이제 잘보일 것이다.

마지막을 플레이를 해볼것인데 애니메이터 컨트롤러를 클릭하고 실행하면

실시간 애니메이션이 변경되며 사용됨을 눈으로 확인 할 수 있다.

애니메이터 실시간.mp4
0.71MB

'산대특' 카테고리의 다른 글

2D Airplane - 원근감있는 무한 배경 만들  (0) 2024.03.17
2D Airplane - 5  (1) 2024.03.14
2D Airplane - 4  (0) 2024.03.12
2D Airplane - 3  (0) 2024.03.12
2D Airplane - 2  (0) 2024.03.10

 

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace yjShin
{
    public class Woman : MonoBehaviour
    {
        private void OnAnimatorIK(int layerIndex)
        {
            Debug.Log("OnAnimatorIK");
        }
    }

}

 

 

 

 

 

오른쪽 팔꿈치의 좌표를 확인

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace yjShin
{
    public class Woman : MonoBehaviour
    {
        private Animator anim;

        private void Start()
        {
            anim = GetComponent<Animator>();
        }
        private void OnAnimatorIK(int layerIndex)
        {
            //Debug.Log("OnAnimatorIK");
            Vector3 rightElbowPosition = anim.GetIKHintPosition(AvatarIKHint.RightElbow);
            Debug.Log(rightElbowPosition);
        }
    }

}

애니메이터에 저장되어 있는 피봇들

 

AvatarIKHint.RightElbow는 애니메이션 시스템에서 제공되는 미리 정의된 상수로, 이를 통해 오른쪽 팔꿈치의 위치를 얻을 수 있습니다. 이 값은 애니메이션 시스템 내부에서 이미 정의되어 있으며, 개발자가 직접 오른쪽 팔꿈치를 찾거나 계산할 필요가 없습니다.

이제 건의 pivot 위치를 잡아 줄 것이다.

 

 [SerializeField] private Transform gunPivot;

 

건의 피봇에 오른쪽 팔꿈치의 위치를 넣어줄 것이다.

건의 피봇의 위치가 팔꿈치에 맞춰졌지만 건의 위치는 다르게 위에 있다.

 

 

건의 위치를 바로 잡아주기 위해

총의 드래그해서 적절한 위치에 이동 시킨후 좌표를 바꿔준다.

 

 

그 후 건의 건을 누르고 복사

 

복사된 좌표를 건에다 붙여준다.

 

 

이제 파지를 할 왼손을 넣어줄 것이다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace smilejsu
{
    public class Woman : MonoBehaviour
    {
        private Animator anim;
        [SerializeField] private Transform gunPivot;
        [SerializeField] private Transform leftHandMount;   //왼쪽 손잡이 

        private void Start()
        {
            this.anim = GetComponent<Animator>();    
        }

        private void OnAnimatorIK(int layerIndex)
        {
            Vector3 rightElbowPosition = anim.GetIKHintPosition(AvatarIKHint.RightElbow);
            gunPivot.position = rightElbowPosition;

            this.anim.SetIKPosition(AvatarIKGoal.LeftHand, this.leftHandMount.position);
            this.anim.SetIKRotation(AvatarIKGoal.LeftHand, this.leftHandMount.rotation);
        }
    }

}

 

그 후 인스펙터에 할당

 

 

 

이제 왼쪽팔의 무게와 회전을 Range로 넣어줄 것이다.

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace yjShin
{
    public class Woman : MonoBehaviour
    {
        private Animator anim;
        [SerializeField] private Transform gunPivot;
        [SerializeField] private Transform leftHandMount; //왼쪽 손잡이

        [Range(0,1)]
        [SerializeField] private float leftHandPositionWeight = 1f;

        [Range(0, 1)]
        [SerializeField] private float leftHandRotationWeight = 1f;

        private void Start()
        {
            anim = GetComponent<Animator>();
        }
        private void OnAnimatorIK(int layerIndex)
        {
            //Debug.Log("OnAnimatorIK");
            Vector3 rightElbowPosition = anim.GetIKHintPosition(AvatarIKHint.RightElbow);
            //Debug.Log(rightElbowPosition);
            gunPivot.position = rightElbowPosition;

            this.anim.SetIKPosition(AvatarIKGoal.LeftHand, this.leftHandMount.position);
            this.anim.SetIKRotation(AvatarIKGoal.LeftHand, this.leftHandMount.rotation);

            this.anim.SetIKPositionWeight(AvatarIKGoal.LeftHand, this.leftHandPositionWeight);
            this.anim.SetIKRotationWeight(AvatarIKGoal.LeftHand, this.leftHandRotationWeight);
        }
    }

}

Animator들의 속성에 대해 모르는 점이 있으면 다큐멘터리를 적극이용하자

https://docs.unity3d.com/ScriptReference/Animator.html

 

Unity - Scripting API: Animator

Success! Thank you for helping us improve the quality of Unity Documentation. Although we cannot accept all submissions, we do read each suggested change from our users and will make updates where applicable. Close

docs.unity3d.com

 

 

그래서 만들어진 완성본

 

 

 

 

 

 

 

using UnityEngine;

// 주어진 Gun 오브젝트를 쏘거나 재장전
// 알맞은 애니메이션을 재생하고 IK를 사용해 캐릭터 양손이 총에 위치하도록 조정
public class PlayerShooter : MonoBehaviour {
    public Gun gun; // 사용할 총
    public Transform gunPivot; // 총 배치의 기준점
    public Transform leftHandMount; // 총의 왼쪽 손잡이, 왼손이 위치할 지점
    public Transform rightHandMount; // 총의 오른쪽 손잡이, 오른손이 위치할 지점

    private PlayerInput playerInput; // 플레이어의 입력
    private Animator playerAnimator; // 애니메이터 컴포넌트

    private void Start() {
        // 사용할 컴포넌트들을 가져오기
        playerInput = GetComponent<PlayerInput>();
        playerAnimator = GetComponent<Animator>();
    }

    private void OnEnable() {
        // 슈터가 활성화될 때 총도 함께 활성화
        gun.gameObject.SetActive(true);
    }
    
    private void OnDisable() {
        // 슈터가 비활성화될 때 총도 함께 비활성화
        gun.gameObject.SetActive(false);
    }

    private void Update() {
        // 입력을 감지하고 총 발사하거나 재장전
        if (playerInput.fire)
        {
            gun.Fire();
        }
        else if (playerInput.reload)
        {
            {
                if (gun.Reload())
                {
                }
                playerAnimator.SetTrigger("Reload");
            }
        }
        UpdateUI();
    }

    // 탄약 UI 갱신
    private void UpdateUI() {
        if (gun != null && UIManager.instance != null)
        {
            // UI 매니저의 탄약 텍스트에 탄창의 탄약과 남은 전체 탄약을 표시
            UIManager.instance.UpdateAmmoText(gun.magAmmo, gun.ammoRemain);
        }
    }

    // 애니메이터의 IK 갱신
    private void OnAnimatorIK(int layerIndex) {

        //총의 기준점 gunPivot을 3D 모델의 오른쪽 팔꿈치 위치로 이동
        gunPivot.position = playerAnimator.GetIKHintPosition(AvatarIKHint.RightElbow);

        //IK를 사용하여 왼손의 위치와 회전을 총의 왼쪽 손잡이에 맞춤
        playerAnimator.SetIKPositionWeight(AvatarIKGoal.LeftHand, 1.0f);
        playerAnimator.SetIKRotationWeight(AvatarIKGoal.LeftHand, 1.0f);

        playerAnimator.SetIKPosition(AvatarIKGoal.LeftHand, leftHandMount.position);
        playerAnimator.SetIKRotation(AvatarIKGoal.LeftHand, leftHandMount.rotation);
        //IK를 사용하여 오른손의 위치와 회전을 총의 오른쪽 손잡이에 맞춤
        playerAnimator.SetIKPositionWeight(AvatarIKGoal.RightHand, 1.0f);
        playerAnimator.SetIKRotationWeight(AvatarIKGoal.RightHand, 1.0f);

        playerAnimator.SetIKPosition(AvatarIKGoal.RightHand, rightHandMount.position);
        playerAnimator.SetIKRotation(AvatarIKGoal.RightHand, rightHandMount.rotation);

    }
}

 

using System.Collections;
using UnityEngine;
using UnityEngine.UIElements;

// 총을 구현
public class Gun : MonoBehaviour
{
    // 총의 상태를 표현하는 데 사용할 타입을 선언
    public enum State
    {
        Ready, // 발사 준비됨
        Empty, // 탄알집이 빔
        Reloading // 재장전 중
    }

    public State state { get; private set; } // 현재 총의 상태

    public Transform fireTransform; // 탄알이 발사될 위치

    public ParticleSystem muzzleFlashEffect; // 총구 화염 효과
    public ParticleSystem shellEjectEffect; // 탄피 배출 효과

    private LineRenderer bulletLineRenderer; // 탄알 궤적을 그리기 위한 렌더러

    private AudioSource gunAudioPlayer; // 총 소리 재생기

    public GunData gunData; // 총의 현재 데이터

    private float fireDistance = 50f; // 사정거리

    public int ammoRemain = 100; // 남은 전체 탄알
    public int magAmmo; // 현재 탄알집에 남아 있는 탄알

    private float lastFireTime; // 총을 마지막으로 발사한 시점

    private void Awake() {
        // 사용할 컴포넌트의 참조 가져오기
        gunAudioPlayer = GetComponent<AudioSource>();
        this.bulletLineRenderer = GetComponent<LineRenderer>();
        this.bulletLineRenderer.positionCount = 2;
        this.bulletLineRenderer.enabled = false;
    }

    private void OnEnable() {
        ammoRemain = gunData.startAmmoRemain;
        magAmmo = gunData.magCapacity;
        // 총 상태 초기화
        this.state = State.Ready;
        lastFireTime = 0;
    }

    // 발사 시도
    public void Fire() {
        if(this.state == State.Ready && Time.time >= lastFireTime + gunData.timeBetFire)
        {
            lastFireTime = Time.time;
            Shot();
        }
    }

    // 실제 발사 처리
    private void Shot() {
        RaycastHit hit;
        Vector3 hitPosition =Vector3.zero;
        //시작점, 방향, 히트정보, 거리
        if(Physics.Raycast(fireTransform.position, fireTransform.forward, out hit, fireDistance))
        {
            //충돌한 콜라이더의 게임오브젝트에 붙어 있는 IDamageable 인터페이스를 상속받고 있는 컴포넌트를 가져온다.
            IDamageable target = hit.collider.GetComponent<IDamageable>();
            if (target != null)
            {
                target.OnDamage(gunData.damage, hit.point, hit.normal);
            }
            hitPosition = hit.point;
        }
        else
        {
            //탄알이 최대 사정거리까지 날아갔을 때의 위치를 충돌 위치로 사용 => out hit가 빠진다.
            hitPosition = fireTransform.position + fireTransform.forward * fireDistance;
        }
        StartCoroutine(ShotEffect(hitPosition));
        magAmmo--;
        if(magAmmo <= 0)
        {
            state = State.Empty;
        }
    }

    // 발사 이펙트와 소리를 재생하고 탄알 궤적을 그림
    private IEnumerator ShotEffect(Vector3 hitPosition) {

        muzzleFlashEffect.Play();
        shellEjectEffect.Play();

        //총격 소리 재생
        gunAudioPlayer.PlayOneShot(gunData.shotClip);

        //라인랜더러의 시작 위치
        this.bulletLineRenderer.SetPosition(0, fireTransform.position);

        //라인랜더러의 끝 위치
        this.bulletLineRenderer.SetPosition(1, hitPosition);

        // 라인 렌더러를 활성화하여 탄알 궤적을 그림
        bulletLineRenderer.enabled = true;

        // 0.03초 동안 잠시 처리를 대기
        yield return new WaitForSeconds(0.03f);

        // 라인 렌더러를 비활성화하여 탄알 궤적을 지움
        bulletLineRenderer.enabled = false;
    }

    // 재장전 시도
    public bool Reload()
    {
        if (state == State.Reloading || ammoRemain <= 0 || magAmmo >= gunData.magCapacity)
        {
            return false;
        }
        StartCoroutine(ReloadRoutine());
        return true;
    }

    // 실제 재장전 처리를 진행
    private IEnumerator ReloadRoutine() {
        // 현재 상태를 재장전 중 상태로 전환
        state = State.Reloading;

        gunAudioPlayer.PlayOneShot(gunData.reloadClip);
        // 재장전 소요 시간 만큼 처리 쉬기
        yield return new WaitForSeconds(gunData.reloadTime);

        int ammoToFill = gunData.magCapacity - magAmmo;
        
        if(ammoRemain < ammoToFill)
        {
            ammoToFill = ammoRemain;
        }
        magAmmo += ammoToFill;
        ammoRemain -= ammoToFill;
        // 총의 현재 상태를 발사 준비된 상태로 변경
        state = State.Ready;
    }

    private void Update()
    {
        //test
        if(Input.GetMouseButtonDown(0))
        {
            this.Fire();
        }
    }

}

 

using UnityEngine;

// 데미지를 입을 수 있는 타입들이 공통적으로 가져야 하는 인터페이스
public interface IDamageable {
    // 데미지를 입을 수 있는 타입들은 IDamageable을 상속하고 OnDamage 메서드를 반드시 구현해야 한다
    // OnDamage 메서드는 입력으로 데미지 크기(damage), 맞은 지점(hitPoint), 맞은 표면의 방향(hitNormal)을 받는다
    void OnDamage(float damage, Vector3 hitPoint, Vector3 hitNormal);
}
using UnityEngine;

[CreateAssetMenu(menuName = "Scriptable/GunData", fileName = "Gun Data")]
public class GunData : ScriptableObject
{
    public AudioClip shotClip; // 발사 소리
    public AudioClip reloadClip; // 재장전 소리

    public float damage = 25; // 공격력

    public int startAmmoRemain = 100; // 처음에 주어질 전체 탄약
    public int magCapacity = 25; // 탄창 용량

    public float timeBetFire = 0.12f; // 총알 발사 간격
    public float reloadTime = 1.8f; // 재장전 소요 시간
}

'산대특 > 게임 클라이언트 프로그래밍' 카테고리의 다른 글

Sprite Shooter - Player and Monster Animation  (0) 2024.03.14
Sprite Shooter - Destroy Barrel and Layer  (0) 2024.03.13
Zombie - Line Render  (0) 2024.03.07
Tank - Rigidbody.MovePosition  (0) 2024.03.06
UniRun  (4) 2024.03.05

1. ground 요소를 추가하여  Rigidbody 2D -> Body Type - Kinematic 설정하여

땅을 만들어 공중에 떠있게 만들어 주었다.

2. 캔버스 레거시 text로 velocityText를 만들어서 고양이가 제자리부터 이동거리를 수치로 표시

    -  ClimbCloudGameDirector 스크립트를 추가하고 CatController에 연결해줌으로 써 이동거리 표시

3. 고양이 선택후 Tool bar에서 Window - Animation- Animation에 들어가서 Create를 누른후 

     Assets안에 애니메이션 폴더를 만들어주고 걷는 애니메이션을 넣을 것으므로 이름을 Walk로 생성

     Animation탭 안에서 add property를 누르고 sprite 선택 후 적적할게 고양이가 나눠져서 걷는 그림을 넣어주었다.

    마지막으로 애니메이션 탭 안 우측 상단에 있는 add keyframe을 누른 후 적당한 거리 뒤에 넣으면

    마지막 프레임이 복사되고 그 프레임 안까지의 사진들이 연속적으로 실행된다.

    애니메이터는 CatController와 같은 위치에 있으므로 따로 호출하지 않아도 클래스 안에서 사용가능

    애니메이션속도를 캐릭터 움직임 속도에 맞춰 변하도록 설정

    this.anim.speed = (Mathf.Abs(this.rbody.velocity.x) / 2f);

 

4. 물리엔진을 사용해서 충돌판정

1. 둘중 하나는 리지드바디 컴포넌트가 있어야 한다

2. 두 객체모두 콜라이더가 있어야 한다

3. isTrigger 모드를 체크 한다

 

 

트리거모드는 업데이트보다 먼저 일어난다

 

5. 깃발에 도달(충돌)했을 때 화면 전환

6. 그후 화면을 터치하면 다시 복귀

7. 고양이 이동가능 거리 조절

8. 고양이가 공중에서 계속 점프하지 못하게 조건문 추가

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.Assertions.Comparers;
using UnityEngine.SceneManagement;

public class CatController : MonoBehaviour
{
    [SerializeField] private Rigidbody2D rbody;
    [SerializeField] private float moveForce = 100f;
    [SerializeField] private float jumpForce = 680f;

    [SerializeField]
    private ClimbCloudGameDirector gameDirector;

    private Animator anim;

    private bool hasSpace = false;

    private void Start()
    {
        //this.gameObject.GetComponent<Animation>();

        anim = GetComponent<Animator>();

        //this.gameDirector = GameObject.Find("GameDirector").GetComponent<ClimbCloudGameDirector>();
        //this.gameDirector = GameObject.FindAnyObjectByType<ClimbCloudGameDirector>();

    }

    void Update()
    {
        //스페이스바를 누르면 
        if (Mathf.Abs(rbody.velocity.y) < 0.01f)

        {
            if (Input.GetKeyDown(KeyCode.Space))
            {
                if (!hasSpace)
                {
                    //힘을 가한다 
                    this.rbody.AddForce(this.transform.up * this.jumpForce);
                    //this.rbody.AddForce(Vector3.up * this.force);

                        hasSpace = false; // 다시 점프할 수 있도록 허용

                }
            }


        }
        // -1, 0, 1 : 방향 
        int dirX = 0;
        //왼쪽화살표키를 누르고 있는 동안에 
        if (Input.GetKey(KeyCode.LeftArrow))
        {
            dirX = -1;
        }

        if (Input.GetKey(KeyCode.RightArrow))
        {
            dirX = 1;
        }

        // Debug.Log(dirX); //방향 -1, 0, 1

        //스케일 X를 변경 하는데 키가 눌렸을 때만 
        //키가 눌렸을때만 = (dirX != 0)
        if (dirX != 0)
        {
            this.transform.localScale = new Vector3(dirX, 1, 1);
        }


        //벡터의 곱 
        //Debug.Log(this.transform.right * dirX);  //벡터3

        //도전 ! : 속도를 제한하자 
        //velocity.x 가 3정도가 넘어가니깐 빨라지는거 같드라구...
        if (Mathf.Abs(this.rbody.velocity.x) < 3)
        {
            this.rbody.AddForce(this.transform.right * dirX * moveForce);
        }

        this.anim.speed = (Mathf.Abs(this.rbody.velocity.x) / 2f);
        this.gameDirector.UpdateVelocityText(this.rbody.velocity);


        // Debug.Log(this.transform.position);

        float clampX = Mathf.Clamp(this.transform.position.x, -2.39f, 2.35f);
        Vector3 pos = this.transform.position;
        pos.x = clampX;
        this.transform.position = pos;


    }

    // Trigger 모드 일경우 충돌 판정을 해주는 이벤트 함수
    private bool hasEntered = false;
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (!hasEntered)
        {
            Debug.LogFormat("OnTriggerEnter2D: {0}", collision);
            SceneManager.LoadScene("ClimbCloudClear");
            hasEntered = true; // 한 번 이벤트가 발생하면 이 변수를 true로 설정하여 두 번 이상 호출되지 않도록 함

        }

    }



}

 

using System.Collections;
using System.Collections.Generic;
using UnityEditor.SearchService;
using UnityEngine;
using UnityEngine.SceneManagement;

public class ChangeScene : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetMouseButton(0))
        {
            
            SceneManager.LoadScene("ClimbCloud");
            Debug.Log("화면이 전환됨");
        }
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class ClimbCloudGameDirector : MonoBehaviour
{
    [SerializeField] private Text velocityText;

    public void UpdateVelocityText(Vector2 velocity)
    {
        float velocityX = Mathf.Abs(velocity.x);
        this.velocityText.text = velocityX.ToString();
    }
}


+ 개선할 점

 

카메라가 고양이 따라가기

'산대특 > 게임 알고리즘' 카테고리의 다른 글

Pirate Bomb - Captain(Enemy)  (0) 2024.02.02
Pirate Bomb - BombGuy  (0) 2024.02.02
ClimbCloud  (2) 2024.02.01
C# 대리자, 람다함수  (1) 2024.01.31
Git, SourceTree  (1) 2024.01.31

+ Recent posts