유튜브 <골드메탈> 영상 시청 및 따라 만들기 - 3D 쿼터뷰 액션게임


추가 및 수정한 스크립트

Player.cs :: 물리 충돌시 각종 버그를 수정하기위헤 FixedUpdate를 추가

1.오브젝트와 충돌시 회전속력이 발생하는 버그를 없애기위해 FreezeRotation()함수 추가

2.벽을 관통하는 버그가 있어 StopToWall() 함수 추가

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

public class Player : MonoBehaviour
{
    bool isBorder = false; //현재 벽과 충돌했는지 체크 유무

    private void Move()
    {
        //정규화를 하는 이유? : 대각선 이동시 빨라지기 때문에 크기(속도)를 1로 만들어서 속도를 똑같이 만들어줘야한다.
        //쉽게 말하면 방향값이 1로 보정되는 벡터라고 생각하면 된다.
        //왜 빨라짐? : hAxis = 1 , vAxis = 1 인경우 대각성 이동시 피타고라스의 법칙으로 root(1^2+1^2) = root(2) = 1.141... 때문이다.
        moveVec = new Vector3(hAxis, 0, vAxis).normalized;

        //무기 교체시 or 무기 공격시 or 재장전시 움직이지 못하게 설정
        if (isSwap || !isFireReady || isReload) moveVec = Vector3.zero;

        //회피 동작 중인경우 회피 방향벡터가 대입되어 방향전환을 못하게 막기 위한 코드
        if (isDodge) moveVec = dodgeVec;

        //transform 이동은 가끔 물리충돌을 무시하는 경우가 발생 함
        //Rigidbody 컴포넌트에서 CollisonDetection을 Continuos로 변경 
        //Cpu를 더 먹어 최적화는 떨어지지만 정확도는 올라가기 때문 

        //위의 조건과 같이 걸어버리면 회전할시에 moveVec에 영향을 받게 되서 캐릭터가 굳어버릴수 있다.
        if (!isBorder)
        { 
            if (!wDown) transform.position += moveVec * speed * Time.deltaTime;
            else transform.position += moveVec * speed * 0.3f * Time.deltaTime;
        }

        //SetXXXX("애니메이션 이름" , bool 변수) 를 넣어 조건문처럼 사용 할 수 있다.
        anim.SetBool("isRun", moveVec != Vector3.zero);
        anim.SetBool("isWalk", wDown);
    }

    private void Turn()
    {
        //지정된 벡터를 향해서 회전시켜주는 함수 - 3D기능
        //현재 플레이어 위치에 방향벡터를 더해서 우리가 나아가는 쪽으로 오브젝트를 회전한다는 뜻
        //#1.키보드에 의한 회전
        transform.LookAt(transform.position + moveVec);

        //#2.마우스에 의한 회전
        if (fDown)
        {
            //스크린 -> 월드로 Ray를 쏘는 함수
            Ray ray = followCamera.ScreenPointToRay(Input.mousePosition);
            RaycastHit rayHit;
            if (Physics.Raycast(ray, out rayHit, 100))
            {
                Vector3 nextVec = rayHit.point - transform.position;
                nextVec.y = 0; //마스크가 없기에 부피가 있는 콜라이더 물체를 클릭시 캐릭터가 빙빙 도는 현상을 막기위해 y =0 추가
                transform.LookAt(transform.position + nextVec);
            }
        }
    }

    void FreezeRotation()
    {
        //rigid.angularVelocity : 물리회전속도
        rigid.angularVelocity = Vector3.zero;
    }

    void StopToWall()
    {
        Debug.DrawRay(transform.position, transform.forward * 5, Color.green);
        //레이어마스크(Wall)과 5 거리 안에 충돌시 isBorder는 true가 될것이다.
        isBorder = Physics.Raycast(transform.position, transform.forward, 5, LayerMask.GetMask("Wall"));
    }

    private void FixedUpdate()
    {
        FreezeRotation();
        StopToWall();
    }


}

 

Item.cs :: 드랍 아이템들의 Player와의 물리충돌을 없애기 위해

1.Floor와 충돌시 Rigidbody를 Kinematic 처리 와 물리충돌을 담당하는 Collider을 비활성화

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;

public class Item : MonoBehaviour
{
    Rigidbody rigid;
    SphereCollider sphereCollider;

    //열거형 변수 enum
    public enum Type
    {
        Ammo, Coin, Grenade, Heart, Weapon
    };

    public Type type; //아이템 종류 변수
    public int value; //아이템 값을 저장할 변수

    private void Awake()
    {
        rigid = GetComponent<Rigidbody>();

        //Item 오브젝트에는 2개의 스피어콜라이더가 있음
        //GetComponent 호출시 Inspector 제일 위에있는 컴포넌트를 호출하게됨
        sphereCollider = GetComponent<SphereCollider>();
    }

    void Update()
    {
        //Rotate(오일러 각도) : 오브젝트를 해당 각도(방향벡터)로 회전시키는 함수 
       transform.Rotate(Vector3.up * 10 * Time.deltaTime);
    }

    //물리효과 변경을 스크립트로 수정
    private void OnCollisionEnter(Collision collision)
    {
        if(collision.gameObject.CompareTag("Floor"))
        {
            //바닥에 충돌시 키네마틱(물리충돌연산x)로 변경
            rigid.isKinematic = true;
            sphereCollider.enabled = false;

        }
    }
}

수정된 물리연산들 & 유니티 기능 정리

1.자동회전방지

탄피에도 RigidBody와 Collider가 있어 탄피가 쌓이고 Player가 위로 올라가면 자동으로 회전하는 버그가 있었다.

이 버그의 원인은 Player <> 총알탄피 의 물리충돌로 인하여 Player의 Rigidbody에 회전속력이 추가되기에 발생되는 것이다.

 

Player.cs

  void FreezeRotation()
    {
        //rigid.angularVelocity : 물리회전속도
        rigid.angularVelocity = Vector3.zero;
    }

해당 코드를 FixedUpdate()에서 호출하여 회전속도를 강제로 0으로 만들어 처리하였다.

 

2.충돌레이어 설정

오브젝트의 레이어를 설정하여 충돌을 원하는데로 컨트롤 할 수 있다.

위 그림과 같이 탄피(BulletCase)와 플레이어 총알(PlayerBullet)의 레이어를 설정하고 충돌 관계를 설정하여 

충돌을 방지했다. 

 

1,2 번으로 문제점을 해결한 결과

1.탄피가 겹치지 않게 설정 완료 , 2.Player가 탄피위에 올라가도 충돌 발생 X

 

3.벽 관통 방지

해당 프로젝트에서 캐릭터가 벽을 관통하는 버그가 있었다. 실제로 3D프로젝트를 진행하면 자주 겪는 버그다.

이 버그를 대처하기 위해서는 RayCast기능을 활용하였다.

Unity Editer에서 확인하기 편하게 해당 코드를 적어준다.

해당 코드는 인게임에서는 보이지 않고 에디터에서만 보이는 레이저다.

    void StopToWall()
    {
        Debug.DrawRay(transform.position, transform.forward * 5, Color.green);
        //레이어마스크(Wall)과 5 거리 안에 충돌시 isBorder는 true가 될것이다.
        isBorder = Physics.Raycast(transform.position, transform.forward, 5, LayerMask.GetMask("Wall"));
    }

isBorder Bool변수를 만들어 레이저가 벽에 닿은 경우 true가 되게 만든뒤 

private void Move()
    {

        //무기 교체시 or 무기 공격시 or 재장전시 움직이지 못하게 설정
        if (isSwap || !isFireReady || isReload) moveVec = Vector3.zero;

        //위의 조건과 같이 걸어버리면 회전할시에 moveVec에 영향을 받게 되서 캐릭터가 굳어버릴수 있다.
        if (!isBorder)
        { 
            if (!wDown) transform.position += moveVec * speed * Time.deltaTime;
            else transform.position += moveVec * speed * 0.3f * Time.deltaTime;
        }

        //SetXXXX("애니메이션 이름" , bool 변수) 를 넣어 조건문처럼 사용 할 수 있다.
        anim.SetBool("isRun", moveVec != Vector3.zero);
        anim.SetBool("isWalk", wDown);
    }

Move()함수에 추가하여 transform에 영향을 안주게 막아버렸다. 이러면 애니메이션은 계속 재생하기에

플레이어가 키를 눌렀다는것을 인지함과 동시에 벽 관통 버그를 원천 봉쇄해버렸다.

 

4.아이템 물리 총돌 제거

물리 충돌을 담당하는 콜라이더와 Player가 충돌시 문제가 발생한다.

충돌시 아이템의 회전속도가 지멋대로 바뀌며 Player는 해당 아이템에 올라 탈 수 있다.

Item.cs

  private void Awake()
    {
        rigid = GetComponent<Rigidbody>();

        //Item 오브젝트에는 2개의 스피어콜라이더가 있음
        //GetComponent 호출시 Inspector 제일 위에있는 컴포넌트를 호출하게됨
        sphereCollider = GetComponent<SphereCollider>();
    }
    
    
    //물리효과 변경을 스크립트로 수정
    private void OnCollisionEnter(Collision collision)
    {
        if(collision.gameObject.CompareTag("Floor"))
        {
            //바닥에 충돌시 키네마틱(물리충돌연산x)로 변경
            rigid.isKinematic = true;
            sphereCollider.enabled = false;

        }
    }


Floor와 충돌할때, Rigid를 키네마틱 , 물리충돌 담당하는 콜라이더를 비활성화 시켜 Player와의 충돌을 방지하였다.


 

 

 

오늘로 사전캠프가 종료되고 9월9일(월)부터 본 캠프가 시작된다.

유튜브 <골드메탈> 영상 시청 및 따라 만들기 - 3D 쿼터뷰 액션게임


추가 및 수정한 스크립트

Player.cs :: 총을 추가해서 해당 마우스 방향으로 총을 발사시 캐릭터가 돌아가게 하는 기능 추가

  private void Turn()
  {
      //지정된 벡터를 향해서 회전시켜주는 함수 - 3D기능
      //현재 플레이어 위치에 방향벡터를 더해서 우리가 나아가는 쪽으로 오브젝트를 회전한다는 뜻
      //#1.키보드에 의한 회전
      transform.LookAt(transform.position + moveVec);

      //#2.마우스에 의한 회전
      if (fDown)
      {
          //스크린 -> 월드로 Ray를 쏘는 함수
          Ray ray = followCamera.ScreenPointToRay(Input.mousePosition);
          RaycastHit rayHit;
          if (Physics.Raycast(ray, out rayHit, 100))
          {
              Vector3 nextVec = rayHit.point - transform.position;
              nextVec.y = 0; //마스크가 없기에 부피가 있는 콜라이더 물체를 클릭시 캐릭터가 빙빙 도는 현상을 막기위해 y =0 추가
              transform.LookAt(transform.position + nextVec);
          }
      }
  }

followCamera는 현재 화면을 비추는 mainCamera를 가지고있다.

스키린화면(우리가 보는 화면)에서 월드(3D 월드스페이스를 말함)로 레이저를 쏘는데 그방향이 '마우스 위치'로 레이저를 쏜다.

RaycastHit는 레이저가 발사해서 맞은 곳의 data를 가져온다. 

마우스의 위치가 저 상태면 바닥의 Floor 오브젝트의 정보를 가져온다.

즉, 캐릭터가 향하는 방향 nextVec를 벡터의 뺼셈을 이용하여 방향벡터를 구하고 

현재 캐릭터의 위치에 방향벡터를 더하여 방향을 수정해준다. 

 


https://www.youtube.com/watch?v=07q9RUTRq4M   

: 원거리 무기 로직 구현 및 총알 오브젝트 및 탄피 생성 방법 

'내일배움캠프_Unity_6기 > TIL(Today I Learend)' 카테고리의 다른 글

TIL : 2024-09-09(월) :: GitHub 기초  (0) 2024.09.09
TIL : 2024-09-07(토)  (0) 2024.09.07
TIL : 2024-09-05(목)  (0) 2024.09.05
TIL : 2024-09-04(수)  (0) 2024.09.04
TIL : 2024-09-03(화)  (0) 2024.09.03

유튜브 <골드메탈> 영상 시청 및 따라 만들기 - 3D 쿼터뷰 액션게임


추가 및 수정한 스크립트

Player.cs ::

1."Item" 태그 오브젝트 충돌시  처리 추가

2."Waepon" 태그 오브젝트 충돌시 처리 추가

3.각종 아이템 갯수 변수 및 아이템 최대갯수 변수 추가

4.Player가 장착하는 무기 배열변수 및 활성화된 무기 bool 변수 추가

5.무기 Swap 기능 근접 무기 공격기능 추가

6.수류탄 공전 연출 추가

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

public class Player : MonoBehaviour
{
    [SerializeField] float speed; //캐릭터의 속도
    [SerializeField] float jumpPower; //캐릭터의 점프파워

    [SerializeField] GameObject[] weapons; // 플레이어가 무기를 들고있을때 활성화 해주는 오브젝트 배열
    [SerializeField] bool[] hasWeapons; // 플레이어가 해당 무기를 끼고있는것츨 체크하는 배열

    //플레이어의 아이팀 소지 갯수 변수
    public int ammo;
    public int coin;
    public int health;
    public int hasGrenades;
    [SerializeField] GameObject[] grenades; //수류탄들을 저장할 배열 변수, 공전 물체를 컨트롤하기 위해 배열로 생성

    //아이템 소지 갯수 최대 변수
    public int maxammo;
    public int maxcoin;
    public int maxhealth;
    public int maxhasGrenades;

    float hAxis; //수평선 축 value값
    float vAxis; //수직선 축 value값

    bool wDown = false; //Input Manager에서 추가한 Walk(Left Shift)의 키 유무 체크
    bool jDown = false; //점프 키 유무 체크
    bool iDown = false; //아이템,무기 획득 키 유무 체크 - 상호작용
    bool fDown = false; //공격 키 유무 체크

    bool isJump = false; //현재 점프 중인지 체크 유무
    bool isDodge = false; //현재 회피 중인지 체크 유무
    bool isSwap = false; //현재 무기 교체 중인지 체크유무 -> 교체할때 다른 액션이 간섭 못하게 하기위해
    bool isFireReady = true; //현재 공격이 가능한 상태를 체크 유무
                      
    //FPS 게임 시 1, 2,3 을누르면 무기 스왑하는 기능으로 설계
    bool sDown1 = false;
    bool sDown2 = false;
    bool sDown3 = false;

    Vector3 moveVec; // hAxis와 vAxis 값을 받아와 3차원 벡터값(방향벡터) 변수
    // 회피시 캐릭터 방향전환을 막기위해 생성
    Vector3 dodgeVec; //회피할때 적용되는 방향벡터
    Animator anim;
    Rigidbody rigid;

    GameObject nearGameObject; // 무기에 근접했을때, 무기 오브젝트를 저장할 변수 
    //GameObject equipWeapon; // 자료형 GameObject -> Weapon으로 변경
    Weapon equipWeapon;  // 현재 장착하고 있는 무기 오브젝트를 저장할 변수 

    //처음 초기값을 -1로 , 무기 첫데이터가 0번 Index이기 때문
    int equipWeaponIndex = -1; // 현재 장착하고 있는 무기의 Index 변수
    float fireDelay; //공격딜레이 변수

    private void Awake()
    {
        rigid = GetComponent<Rigidbody>();
        anim = GetComponentInChildren<Animator>();
    }

    private void Update()
    {
        GetInput();
        Move();
        Turn();
        Jump();
        Attack();
        Dodge();
        Interation();
        Swap();
    }

    private void GetInput()
    {
        //해당 프로젝트 InputManager를 이용하여 만듬 

        //GetAxisisRaw() : Axis 값을 정수로 반환하는 함수
        //GetButton() : 꾹 누르고 있을때만 호출
        hAxis = Input.GetAxisRaw("Horizontal");
        vAxis = Input.GetAxisRaw("Vertical");
        wDown = Input.GetButton("Walk");
        jDown = Input.GetButtonDown("Jump");
        fDown = Input.GetButtonDown("Fire1");
        iDown = Input.GetButtonDown("Interation");
        sDown1 = Input.GetButtonDown("Swap1");
        sDown2 = Input.GetButtonDown("Swap2");
        sDown3 = Input.GetButtonDown("Swap3");
    }

    private void Move()
    {
        //정규화를 하는 이유? : 대각선 이동시 빨라지기 때문에 크기(속도)를 1로 만들어서 속도를 똑같이 만들어줘야한다.
        //쉽게 말하면 방향값이 1로 보정되는 벡터라고 생각하면 된다.
        //왜 빨라짐? : hAxis = 1 , vAxis = 1 인경우 대각성 이동시 피타고라스의 법칙으로 root(1^2+1^2) = root(2) = 1.141... 때문이다.
        moveVec = new Vector3(hAxis, 0, vAxis).normalized;

        //무기 교체시 or 무기 공격시 움직이지 못하게 설정
        if (isSwap || !isFireReady) moveVec = Vector3.zero;

        //회피 동작 중인경우 회피 방향벡터가 대입되어 방향전환을 못하게 막기 위한 코드
        if (isDodge) moveVec = dodgeVec;

        //transform 이동은 가끔 물리충돌을 무시하는 경우가 발생 함
        //Rigidbody 컴포넌트에서 CollisonDetection을 Continuos로 변경 
        //Cpu를 더 먹어 최적화는 떨어지지만 정확도는 올라가기 때문 

        if (!wDown) transform.position += moveVec * speed * Time.deltaTime;
        else transform.position += moveVec * speed * 0.3f * Time.deltaTime;

        //SetXXXX("애니메이션 이름" , bool 변수) 를 넣어 조건문처럼 사용 할 수 있다.
        anim.SetBool("isRun", moveVec != Vector3.zero);
        anim.SetBool("isWalk", wDown);
    }

    private void Turn()
    {
        //지정된 벡터를 향해서 회전시켜주는 함수 - 3D기능
        //현재 플레이어 위치에 방향벡터를 더해서 우리가 나아가는 쪽으로 오브젝트를 회전한다는 뜻
        transform.LookAt(transform.position + moveVec);
    }

    //점프키와 회피키가 같지만 이동키가 눌리고 있을때는 회피, 이동 안할때는 점프를 하게된다.
    private void Jump()
    {
        //방향벡터가 0 이기에 이동 하지 않으므로 점프
        if(jDown && moveVec == Vector3.zero && !isJump && !isDodge && !isSwap) //액션 도중에 다른 액션이 실행되지 않도록 조건 추가
        {
            //Addforce : rigidbody에 포함된 메서드이며 해당 오브젝트에 힘을 가해주는 함수
            rigid.AddForce(Vector3.up * jumpPower,ForceMode.Impulse); // Impulse : 힘을 주는 종류 중 하나로, 즉발로 힘을 준다는 의미
            anim.SetBool("isJump",true);
            anim.SetTrigger("doJump");
            isJump = true;
        }
    }

    private void Attack()
    {
        if (equipWeapon == null) //무기가 있을떄만 실행되도록 체크
        {
            return;
        }

        fireDelay += Time.deltaTime;
        isFireReady = equipWeapon.rate < fireDelay; //무기에 있는 공격속도 보다 현재 딜레이가 크면 공격 가능상태로 변경

        //공격키가 눌렸고 (fDonw) , 공격 준비 상태(isFireReady)고 점프 액션을 제외한 액션이 동작안할때 조건
        if(fDown && isFireReady && !isSwap && !isDodge)
        {
            equipWeapon.Use(); //장착하고 있는 무기의 Weapon Use함수 호출
            anim.SetTrigger("doSwing");
            fireDelay = 0; //공격을 했기에 공격딜레이 초기화
        }
    }

    private void Dodge()
    {
        //방향벡터가 0 이 아니므로 이동 하는 상태이기 때문에 회피
        if (jDown && moveVec != Vector3.zero && !isJump && !isDodge && !isSwap) //액션 도중에 다른 액션이 실행되지 않도록 조건 추가
        {
            dodgeVec = moveVec;  //회피시 현재 이동벡터를 회피벡터에 대입한뒤 Move()함수에서 대신 사용됨
            speed *= 2;
            anim.SetTrigger("doDodge");
            isDodge = true;

            Invoke("DodgeOut",0.4f);
        }
    }

    private void DodgeOut()//회피가 끝났을때
    {
        speed *= 0.5f;
        isDodge = false;
    }

    private void SwapOut()//교체가 끝났을때
    {
        isSwap = false;
    }

    private void Swap() //무기 교체
    {
        //무기 스왑키를 눌렀을때
        //1.무기를 같은 무기로 교체 안되게 함
        //2.획득하지 않은 무기는 교체를 못하게 하기위해
        //이 두가지 동작을 막기위한 로직 추가 
        if(sDown1 && (!hasWeapons[0] || equipWeaponIndex == 0))
        {
            return;
        }
        if (sDown2 && (!hasWeapons[1] || equipWeaponIndex == 1))
        {
            return;
        }
        if (sDown3 && (!hasWeapons[2] || equipWeaponIndex == 2))
        {
            return;
        }

        //무기 인덱스를 초기화 하고 키 누르는것을 확인하고 인덱스 값 변경
        int weaponIndex = -1;
        if (sDown1) weaponIndex = 0;
        if (sDown2) weaponIndex = 1;
        if (sDown3) weaponIndex = 2;

        //액션중에는 무기 변경이 안되게 조건문을 설정
        if ((sDown1 || sDown2 || sDown3) && !isJump && !isDodge)
        {
            if (equipWeapon != null) //빈손이 아닌경우에만
                equipWeapon.gameObject.SetActive(false); //빈손이 아닌경우 장착 무기 비활성화

            equipWeaponIndex = weaponIndex;
            equipWeapon = weapons[weaponIndex].GetComponent<Weapon>(); // 장착할 무기 저장
            equipWeapon.gameObject.SetActive(true); //장착 무기 활성화

            anim.SetTrigger("doSwap");

            isSwap = true;
            Invoke("SwapOut", 0.4f);//애니메이션이 안보여서 지연시간을 줌 
        }
    }

    private void Interation() //아이팀과 무기를 상호작용하는 하는 함수 (조작키  : E)
    {   
        //상호작용키가 눌리고(idown) ,무기오브젝트가 비어있지 않고(nearGameObject) , 액션중에 동작이 안되게하는 조건
        if(iDown && nearGameObject != null && !isJump && !isDodge)
        {
            if(nearGameObject.gameObject.CompareTag("Weapon"))
            {
                //nearGameObject 의 Item 컴포넌트를 가져와서 저장
                Item item = nearGameObject.GetComponent<Item>();
                int weaponIndex = item.value; //해당 아이템의 Value값을 호출
                hasWeapons[weaponIndex] = true; //bool 배열에 해당 무기를 장착했다고 알림

                Destroy(nearGameObject); //무기를 획득했기에 해ㅔ당 오브젝트를 파괴 -> Player내에서 파괴한게아니라 WorldSpace에서의 파괴
            }
        }
    }

    private void OnCollisionEnter(Collision collision)
    {
        //충돌처리 함수로 점프 후 착지 확인
        if(collision.gameObject.CompareTag("Floor"))
        {
            anim.SetBool("isJump", false);
            isJump = false;
        }
    }

    private void OnTriggerEnter(Collider other)
    {
        if(other.gameObject.CompareTag("Item"))
        {
            //충돌된 오브젝트의 Item 컴포넌트를 호출
            Item item = other.GetComponent<Item>();

            //호출한 Item의 value값을 호출하여 조건문 진행
            switch (item.type)
            {
                case Item.Type.Ammo:
                    ammo += item.value;
                    if(ammo > maxammo) ammo = maxammo;

                    break;

                case Item.Type.Coin:
                    coin += item.value;
                    if (coin > maxcoin) coin = maxcoin;
   
                    break;

                case Item.Type.Grenade:
                
                    hasGrenades += item.value;
                    if (hasGrenades > maxhasGrenades) hasGrenades = maxhasGrenades;

                    //Player가 몇개의 수류탄을 가지고 잇는지 연출 설계
                    //Player 주위로 수류탄이 공전하는 공전물체 생성으로 갯수 파악
                    grenades[hasGrenades - 1].SetActive(true);

                    break;

                case Item.Type.Heart:
                    health += item.value;
                    if (health > maxhealth) health = maxhealth;
          
                    break;
            }
            //사용된 아이템 오브젝트는 파괴
            Destroy(other.gameObject);

        }

   
    }

    private void OnTriggerStay(Collider other)
    {
        //Player가 무기에 근접했다는것을 체크
        if(other.gameObject.CompareTag("Weapon"))
        {
            //근접한 무기 오브젝트를 저장
            nearGameObject = other.gameObject;
        }
    }

    private void OnTriggerExit(Collider other)
    {
        //Player가 무기에서 벗어났다는것을 체크
        if (other.gameObject.CompareTag("Weapon"))
        {
            //벗어낫기에 오브젝트를 null로 초기화 
            nearGameObject = null;
        }
    }
}

 

Item.cs :: 드랍되있는 아이템들은 전부 Item.cs를 따른다. 

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

public class Item : MonoBehaviour
{
    //열거형 변수 enum
    public enum Type
    {
        Ammo, Coin, Grenade, Heart, Weapon
    };

    public Type type; //아이템 종류 변수
    public int value; //아이템 값을 저장할 변수 ex)회복량,총알량,무기의 코드번호 등등..

    void Update()
    {
        //Rotate(오일러 각도) : 오브젝트를 해당 각도(방향벡터)로 회전시키는 함수 
       transform.Rotate(Vector3.up * 10 * Time.deltaTime);
    }
}

 

Orbit.cs :: 물체를 타겟 주위로 공전시키는 스크립트

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

//수류탄이 공전하는 스크립트
public class Orbit : MonoBehaviour
{
    public Transform target;
    public float orbitSpeed;
    Vector3 offset;

    private void Start()
    {
        offset = transform.position - target.position;
    }

    void Update()
    {
        transform.position = target.position + offset;

        //타겟주위를 회전하는 함수 , 단점 : 목표(target)이 움직이면 일그러져서 회전 반경이 이상하게 됨
        //이 단점을 대처하기위해 Offset 변수 설정
        transform.RotateAround(target.position,
                               Vector3.up,
                               orbitSpeed * Time.deltaTime);

        //RotateAround() 후의 위치를 가지고 목표와의 거리를 유지
        offset = transform.position - target.position;
    }
}

 

Weanpon.cs :: 근접무기만 구현되있는 상태이며, 이 Weapon 스크립트는 드랍된 오브젝트가 아닌 Player 오브젝트에 자식으로 있는 무기 오브젝트들만 가지고 있다. 

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

public class Weapon : MonoBehaviour
{
    public enum Type { Melee, Range } //근거리, 원거리
    public Type type;//무기타입
    public int damage;//공격력
    public float rate;//공격속도
    public BoxCollider meleeArea; //근접 공격범위 콜라이더
    public TrailRenderer trailRenderer; //근정 공격시 연출 변수


    //Player에서 호출할것이기에 public으로 접근한정자 설정
    public void Use()
    {
        if(type == Type.Melee)
        {
            //똑같은 코루틴을 동작할때 Stop으로 멈추게하고 동작 해야만한다.(설계상 그렇다는거지 이걸 노리고 할수도 있음)
            //비동기로 돌아가기때문에 Start로 하면 코루틴이 2개 돌아가 로직이 꼬여버릴 수 있다.
            StopCoroutine(Swing());
            StartCoroutine(Swing());
        }
    }

    //열거형 함수 클래스 :: IEnumerator
    IEnumerator Swing()
    {
        //yield 결과를 전달하는 키웓 :: yield
        yield return new WaitForSeconds(0.1f);
        //근접공격 콜라이더와 트레일렌더러를 활성화 시킴
        meleeArea.enabled = true;
        trailRenderer.enabled = true;

        //근접공격을 하고 콜라이더를 바로 꺼버리면 공격이 안될 수도있으니
        //지연시간을 줘서 공격 콜라이더의 수명을 늘리기
        yield return new WaitForSeconds(rate);
        meleeArea.enabled = false;

        //근접공격의 연출을 바로 종료하지않고 지연시간을 줘서 기다리게 하고 종료
        yield return new WaitForSeconds(0.1f);
        trailRenderer.enabled = false;
    }
}

유니티 기능 정리

1.드랍된 오브젝트(Player 몸에 장착되는 오브젝트)의 콜라이더 관리

Player 가 획득시 손에 장착되는 망치(무기) 오브젝트

 

1-1.빨간상자 의 Collider는 땅위에 서있어야 되기때문에 추가

RigidBody를 적용시켜 중력을 받게 해야됨

 

1-2.파란상자의 Collider는 Player가 충돌시 Player의 스크립트 안에 미리 무기를 저장시키는 콜라이더이다.

무기를 완전하게 획득시 해당 오브젝트의 Item 컴포넌트를 호출한다.

public class Player : MonoBehaviour
{
    [SerializeField] GameObject[] weapons; // 플레이어가 무기를 들고있을때 활성화 해주는 오브젝트 배열
    [SerializeField] bool[] hasWeapons; // 플레이어가 해당 무기를 끼고있는것츨 체크하는 배열

   GameObject nearGameObject; // 무기에 근접했을때, 무기 오브젝트를 저장할 변수 
 	 
    private void Interation() //무기(item)와 상호작용하는 하는 함수 (조작키  : E)
    {   
        //상호작용키가 눌리고(idown) ,무기오브젝트가 비어있지 않고(nearGameObject) , 액션중에 동작이 안되게하는 조건
        if(iDown && nearGameObject != null && !isJump && !isDodge)
        {
            if(nearGameObject.gameObject.CompareTag("Weapon"))
            {
                //nearGameObject 의 Item 컴포넌트를 가져와서 저장
                Item item = nearGameObject.GetComponent<Item>();
                int weaponIndex = item.value; //해당 아이템의 Value값을 호출
                hasWeapons[weaponIndex] = true; //bool 배열에 해당 무기를 장착했다고 알림

                Destroy(nearGameObject); //무기를 획득했기에 해당 오브젝트를 파괴
                //Player내에서 파괴한게아니라 WorldSpace에서의 파괴
            }
        }
    }
     
     
   private void OnTriggerStay(Collider other)
    {
        //Player가 무기에 근접했다는것을 체크
        if(other.gameObject.CompareTag("Weapon"))
        {
            //근접한 무기 오브젝트를 저장
            nearGameObject = other.gameObject;
        }
    }

    private void OnTriggerExit(Collider other)
    {
        //Player가 무기에서 벗어났다는것을 체크
        if (other.gameObject.CompareTag("Weapon"))
        {
            //벗어낫기에 오브젝트를 null로 초기화 
            nearGameObject = null;
        }
    }
}

Player.cs에서 해당되는 부분의 스크립트만 가져왔다.

 

OnTriggerStay/Exit 함수로 근처에 있는 무기(Item)오브젝트를 저장 및 초기화를 하고있다.

그리고 무기 오브젝트에 다가가서 <E>키를 누르면 Interation()함수가 호출되어 해당 오브젝트(무기)를 Player 오브젝트가 가지고 있다고 판단을 한다.

 

2.무기 오브젝트를 획득할때 스크립트로 무기를 넣어 오브젝트를 생성해줘야함?

물론, 그렇게 해도 된다. 하지만, 이번 프로젝트에서는 다른 방법으로 처리를 하였다.

 

우선 무기오브젝트를 장착할 때, 어떡해 하는지를 메모하겠다.

Player의 오른팔에 무기를 장착하기 위해 RightHand(자식) 으로 빈 오브젝트를 넣어 관리 할거다.

보통 3D게임에서 무기를 관리할떄 이 방법을 많이 사용하는것 같다. 

우선 오른손의 무기 잡는 위치를 조정해줘야한다. 

자식으로 실린더 오브젝트를 만들고 위치를 손 안으로 집어넣어서 위치 조정

손 안으로 실린더를 집어넣어 위치를 잡아주고 실린더는 빈오브젝트 처럼 보여야 되기 땜누에 Renderer 및 Collider를 비활성화 제거를 해준다.

실린더 오브젝트 -> Weapon Point 로 변경하여 무기가 위치 되는 되는곳으로 잡아준다.

해당 오브젝트 자식으로 무기 오브젝트들을 전부 자식으로 넣은뒤 비 활성화 해주고, 이 오브젝트들을 최상위 부모 클래스인 Player의 Inspector를 통해 Weapons 배열에 삽입해준다.

무기를 획득하여 Inspector에 체크가 되어있다.

1번과 2번을 통해 만들면 위 그림 처럼 완성된다. 

 

 

3.공전하는 물체 오브젝트 만들기 및 유니티 에디터에 표시하기

해당 프로젝트에서 수류탄이라는 아이템을 획득시 UI 표현이 아닌 Player 캐릭터 주위로 돌리는것으로 판단하기로 했다.

수류탄 회전 위치를 표시하기 위해 아이콘을 설정하면 이렇게 표시된다.

//수류탄이 공전하는 스크립트
public class Orbit : MonoBehaviour
{
    public Transform target;
    public float orbitSpeed;
    Vector3 offset;

    private void Start()
    {
        offset = transform.position - target.position;
    }

    void Update()
    {
        transform.position = target.position + offset;

        //타겟주위를 회전하는 함수 , 단점 : 목표(target)이 움직이면 일그러져서 회전 반경이 이상하게 됨
        //이 단점을 대처하기위해 Offset 변수 설정
        transform.RotateAround(target.position,
                               Vector3.up,
                               orbitSpeed * Time.deltaTime);

        //RotateAround() 후의 위치를 가지고 목표와의 거리를 유지
        offset = transform.position - target.position;
    }
}

 

Orbit.cs는 RotateAround() 함수를 통해서 Player 오브젝트 주위로 y 축을 회전 시키는 스크립트다.

하지만 RotateAround()를 사용하게되면 target 오브젝트가 이동시 회전 반경이 일그러지기 때문에 따로 보정이 필요하다.

 

Orbit.cs를 가지고 있는 transform.position 과 target.position의 차이를 구하고 회전 반경을 구할때 다시 offset를 더해서 계속 보정하는 로직을 가지고있다. 

 

또한 Player에서 수류탄을 획득한 갯수만큼 보여주게 하는 로직이 필요한데

public class Player : MonoBehaviour
{
	 public int hasGrenades;
 	[SerializeField] GameObject[] grenades; //수류탄들을 저장할 배열 변수, 공전 물체를 컨트롤하기 위해 배열로 생성
 
	 private void OnTriggerEnter(Collider other)
 	{
         if(other.gameObject.CompareTag("Item"))
         {
             //충돌된 오브젝트의 Item 컴포넌트를 호출
             Item item = other.GetComponent<Item>();

             //호출한 Item의 value값을 호출하여 조건문 진행
             switch (item.type)
             {
                 case Item.Type.Grenade:

                     hasGrenades += item.value;
                     if (hasGrenades > maxhasGrenades) hasGrenades = maxhasGrenades;

                     //Player가 몇개의 수류탄을 가지고 잇는지 연출 설계
                     //Player 주위로 수류탄이 공전하는 공전물체 생성으로 갯수 파악
                     grenades[hasGrenades - 1].SetActive(true);

                     break;
             }
             //사용된 아이템 오브젝트는 파괴
             Destroy(other.gameObject);

         }
 	 }

}

OnTriggerEnter() 아이템을 획득 할 때마다 해당 오브젝트의 수류탄 Mesh를 활성화 시켜주는 로직이다. 

 

4.TrailRenderer 컴포넌트의 기능

TrailRenderer는 오브젝트의 잔상을 그려주는 기능이다.

player의 자식 오브젝트(근접무기)의 자식 오브젝트에 Effect로 추가하여 관리한다.

 

TrailRenderer 기능 설명

 

5.Particle System 기능 설명

여기서 설명하는 Particle System 기능은 이동시에 파티클을 생성하는 기능을 설정하는 방법이다.

 

(좌)Simulation Space - Local 일때, 파티클이 부모오브젝트를 따라온다. (우)Simulation Space - World 일때, 파티클이 World Space 기준으로 따라온다.

 

6.Floor의 Material 재질 타일링 하는법

해당 오브젝트에 적용되는 Material 설정을 건들여야 한다.

Floor의 텍스쳐가 타일링 되어있다.

 


2024-09-05(목)

https://www.youtube.com/watch?v=u2DLOay5oO8&t=738s

: 아이템 로직 및 프리팹 저장  (12:20초까지 수강)

https://www.youtube.com/watch?v=APS9OY_p6wo

: 드랍 무기 획득 및 무기교체(FPS 무기의 1,2,3)

https://www.youtube.com/watch?v=esGkgvm9eSg

: 아이템 먹기 & 공전물체 만들기(획득한 수류탄 갯수 파악 겸 제작)

https://www.youtube.com/watch?v=Zfoyagdz1y0   

: 코루틴으로 근접공격 구현 및 공격 범위 , 잔상연출 추가 

 

 

유니티로 3D 다뤄본적이 별로 없기에 유튜브 강의 하나를 그대로 따라하려 한다.

 

https://www.youtube.com/watch?v=WkMM7Uu2AoA&list=PLO-mt5Iu5TeYkrBzWKuTCl6IUm_bA6BKy

유니티 강의를 많이 올려주시는 <골드메탈> 유튜브분의 영상인데 4년전 영상이지만

따라 만들어보면 실력이 많이 늘것 같아 시도해보려한다.


추가 및 수정한 스크립트

Player.cs

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

public class Player : MonoBehaviour
{
    [SerializeField] float speed; //캐릭터의 속도
    [SerializeField] float jumpPower; //캐릭터의 점프파워
    float hAxis; //수평선 축 value값
    float vAxis; //수직선 축 value값
    bool wDown = false; //Input Manager에서 추가한 Walk(Left Shift)의 키 유무 체크
    bool jDown = false; //점프 키 유무 체크

    bool isJump = false; //현재 점프 중인지 체크 유무
    bool isDodge = false; //현재 회피 중인지 체크 유무

    Vector3 moveVec; // hAxis와 vAxis 값을 받아와 3차원 벡터값(방향벡터) 변수
    // 회피시 캐릭터 방향전환을 막기위해 생성
    Vector3 dodgeVec; //회피할때 적용되는 방향벡터
    Animator anim;
    Rigidbody rigid;

    private void Awake()
    {
        rigid = GetComponent<Rigidbody>();
        anim = GetComponentInChildren<Animator>();
    }

    private void Update()
    {
        GetInput();
        Move();
        Turn();
        Jump();
        Dodge();
    }

    private void GetInput()
    {
        //해당 프로젝트 InputManager를 이용하여 만듬 

        //GetAxisisRaw() : Axis 값을 정수로 반환하는 함수
        //GetButton() : 꾹 누르고 있을때만 호출
        hAxis = Input.GetAxisRaw("Horizontal");
        vAxis = Input.GetAxisRaw("Vertical");
        wDown = Input.GetButton("Walk");
        jDown = Input.GetButtonDown("Jump");
    }

    private void Move()
    {
        //정규화를 하는 이유? : 대각선 이동시 빨라지기 때문에 크기(속도)를 1로 만들어서 속도를 똑같이 만들어줘야한다.
        //쉽게 말하면 방향값이 1로 보정되는 벡터라고 생각하면 된다.
        //왜 빨라짐? : hAxis = 1 , vAxis = 1 인경우 대각성 이동시 피타고라스의 법칙으로 root(1^2+1^2) = root(2) = 1.141... 때문이다.
        moveVec = new Vector3(hAxis, 0, vAxis).normalized;

        //회피 동작 중인경우 회피 방향벡터가 대입되어 방향전환을 못하게 막기 위한 코드
        if(isDodge) moveVec = dodgeVec;

        //transform 이동은 가끔 물리충돌을 무시하는 경우가 발생 함
        //Rigidbody 컴포넌트에서 CollisonDetection을 Continuos로 변경 
        //Cpu를 더 먹어 최적화는 떨어지지만 정확도는 올라가기 때문 

        if (!wDown) transform.position += moveVec * speed * Time.deltaTime;
        else transform.position += moveVec * speed * 0.3f * Time.deltaTime;

        //SetXXXX("애니메이션 이름" , bool 변수) 를 넣어 조건문처럼 사용 할 수 있다.
        anim.SetBool("isRun", moveVec != Vector3.zero);
        anim.SetBool("isWalk", wDown);
    }

    private void Turn()
    {
        //지정된 벡터를 향해서 회전시켜주는 함수 - 3D기능
        //현재 플레이어 위치에 방향벡터를 더해서 우리가 나아가는 쪽으로 오브젝트를 회전한다는 뜻
        transform.LookAt(transform.position + moveVec);
    }

    //점프키와 회피키가 같지만 이동키가 눌리고 있을때는 회피, 이동 안할때는 점프를 하게된다.
    private void Jump()
    {
        //방향벡터가 0 이기에 이동 하지 않으므로 점프
        if(jDown && moveVec == Vector3.zero && !isJump && !isDodge) //액션 도중에 다른 액션이 실행되지 않도록 조건 추가
        {
            //Addforce : rigidbody에 포함된 메서드이며 해당 오브젝트에 힘을 가해주는 함수
            rigid.AddForce(Vector3.up * jumpPower,ForceMode.Impulse); // Impulse : 힘을 주는 종류 중 하나로, 즉발로 힘을 준다는 의미
            anim.SetBool("isJump",true);
            anim.SetTrigger("doJump");
            isJump = true;
        }
    }

    private void Dodge()
    {
        //방향벡터가 0 이 아니므로 이동 하는 상태이기 때문에 회피
        if (jDown && moveVec != Vector3.zero && !isJump && !isDodge) //액션 도중에 다른 액션이 실행되지 않도록 조건 추가
        {
            dodgeVec = moveVec;  //회피시 현재 이동벡터를 회피벡터에 대입한뒤 Move()함수에서 대신 사용됨
            speed *= 2;
            anim.SetTrigger("doDodge");
            isDodge = true;

            Invoke("DodgeOut",0.4f);
        }
    }

    private void DodgeOut()//회피가 끝났을때
    {
        speed *= 0.5f;
        isDodge = false;
    }

    private void OnCollisionEnter(Collision collision)
    {
        //충돌처리 함수로 점프 후 착지 확인
        if(collision.gameObject.CompareTag("Floor"))
        {
            anim.SetBool("isJump", false);
            isJump = false;
        }
    }
}

 

Follow.cs :: 카메라가 등록한 Target(오브젝트)를 따라다니는 기능

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

//카메라가 플레이어를 따라가는 기능
public class Follow : MonoBehaviour
{
    public Transform target; // 따라갈 오브젝트
    public Vector3 offset; // 위치 오프셋(카메라를 배치할 위치 )

    // Update is called once per frame
    void Update()
    {
        transform.position = target.position + offset;
    }
}

 

Item.cs 

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

public class Item : MonoBehaviour
{
    //열거형 변수 enum
    public enum Type
    {
        Ammo, Coin, Grenade, Heart, Weapon
    };

    public Type type; //아이템 종류 변수
    public int value; //아이템 값을 저장할 변수
}

1.정규화(Nomarlize)를 하는이유

 

벡터크기와 방향값을 둘다 가지는 단위이다.

간단히 말하면 정규화라는 것은 해당 벡터의 크기를 1로 변환하여 순수한 방향값을 얻을때 주로 사용된다.

 //정규화를 하는 이유? : 대각선 이동시 빨라지기 때문에 크기(속도)를 1로 만들어서 속도를 똑같이 만들어줘야한다.
 //쉽게 말하면 방향값이 1로 보정되는 벡터라고 생각하면 된다.
 //왜 빨라짐? : hAxis = 1 , vAxis = 1 인경우 대각성 이동시 피타고라스의 법칙으로 root(1^2+1^2) = root(2) = 1.141... 때문이다.
 moveVec = new Vector3(hAxis, 0, vAxis).normalized;

 

해당 코드에서 대각선으로 이동시 캐릭터가 더 빠르게 움직이게 된다.

피타고라스 법칙으로 hValue = 1, vValue =1 인경우 대각선 이동시 1.141.. 로 빠르게 이동된다.

그렇기 때문에 해당 벡터의 크기를 1로 정규화를 하면 순수한 방향벡터값을 구할 수 있다.

 

1시방향으로 대각선 이동 - 방향벡터
1시방향으로 대각선 이동 - 정규화 된 방향벡터

 

2. 하나의 키로 두가지 동작하기 (점프와 회피)  + 회피시 방향전환 못하게 막기 

 bool isDodge = false; //현재 회피 중인지 체크 유무

 Vector3 moveVec; // hAxis와 vAxis 값을 받아와 3차원 벡터값(방향벡터) 변수
 // 회피시 캐릭터 방향전환을 막기위해 생성
 Vector3 dodgeVec; //회피할때 적용되는 방향벡터

 private void GetInput()
 {
     //해당 프로젝트 InputManager를 이용하여 만듬 

     //GetAxisisRaw() : Axis 값을 정수로 반환하는 함수
     //GetButton() : 꾹 누르고 있을때만 호출
     hAxis = Input.GetAxisRaw("Horizontal");
     vAxis = Input.GetAxisRaw("Vertical");
     wDown = Input.GetButton("Walk");
     jDown = Input.GetButtonDown("Jump");
 }

private void Move()
{
	////////
     moveVec = new Vector3(hAxis, 0, vAxis).normalized;
 	Debug.Log("정규화된 벡터 : " + moveVec);
    
    //회피 동작 중인경우 회피 방향벡터가 대입되어 방향전환을 못하게 막기 위한 코드
    if (isDodge) moveVec = dodgeVec;

    ///////
}

//점프키와 회피키가 같지만 이동키가 눌리고 있을때는 회피, 이동 안할때는 점프를 하게된다.
private void Jump()
{
    //방향벡터가 0 이기에 이동 하지 않으므로 점프
    if(jDown && moveVec == Vector3.zero && !isJump && !isDodge) //액션 도중에 다른 액션이 실행되지 않도록 조건 추가
    {
        //Addforce : rigidbody에 포함된 메서드이며 해당 오브젝트에 힘을 가해주는 함수
        rigid.AddForce(Vector3.up * jumpPower,ForceMode.Impulse); // Impulse : 힘을 주는 종류 중 하나로, 즉발로 힘을 준다는 의미
        anim.SetBool("isJump",true);
        anim.SetTrigger("doJump");
        isJump = true;
    }
}

private void Dodge()
{
    //방향벡터가 0 이 아니므로 이동 하는 상태이기 때문에 회피
    if (jDown && moveVec != Vector3.zero && !isJump && !isDodge) //액션 도중에 다른 액션이 실행되지 않도록 조건 추가
    {
        dodgeVec = moveVec;  //회피시 현재 이동벡터를 회피벡터에 대입한뒤 Move()함수에서 대신 사용됨
        speed *= 2;
        anim.SetTrigger("doDodge");
        isDodge = true;

        Invoke("DodgeOut",0.4f);
    }
}

Player.cs에 있는 코드를 가져왔다.

 

*하나의 키로 두가지 동작하기 (점프와 회피) 

해당 프로젝트는 Player의 점프 키와 회피키를 같은것을 사용하고 있다.

if로 조건문을 걸어 각 액션동작 중에 다른 동작을 하지 못하게 막아놨다.

 

moveVec가 0이라는 것은 GetInput() 함수에서 키 입력이 없기에 0을 리턴한것이고

즉, 이동하지 않았다는 것이기 때문에 moveVec == Vector3.zero 로 조건을 걸어 각 액션시 동작을 다르게 하였다.

 

* 회피시 방향전환 못하게 막기 

moveVec가 아닌 회피할때만 사용하는 방향벡터를 만들면 해결되는 문제이기에

dodgeVec를 생성하고 회피행동시 dodgeVec = moveVec를 선언하고

현재 회피중 일때 (isdodge 가 true) moveVec 를 계속 dodgeVec로 초기화해주는 것이다. 

 

3.이번 프로젝트에서 사용된 모르는 함수들 정리

  GetAxisisRaw() : Axis 값을 정수로 반환하는 함수
  GetButton() : 꾹 누르고 있을때만 호출
  
  ex)
      hAxis = Input.GetAxisRaw("Horizontal");
      vAxis = Input.GetAxisRaw("Vertical");
      wDown = Input.GetButton("Walk");
      jDown = Input.GetButtonDown("Jump");
 
 /*=========================================================*/
      
SetXXXX("애니메이션 이름" , bool 변수) 를 넣어 조건문처럼 사용 할 수 있다.
ex)
 	anim.SetBool("isRun", moveVec != Vector3.zero);
 	anim.SetBool("isWalk", wDown);

 /*=========================================================*/

transform.LookAt(Vector3 vec) : 지정된 벡터를 향해서 회전시켜주는 함수 - 3D기능
ex)
	//현재 플레이어 위치에 방향벡터를 더해서 우리가 나아가는 쪽으로 오브젝트를 회전한다는 뜻
	transform.LookAt(transform.position + moveVec);
    
/*=========================================================*/

Addforce : rigidbody에 포함된 메서드이며 해당 오브젝트에 힘을 가해주는 함수
ex)
	// Impulse : 힘을 주는 종류 중 하나로, 즉발로 힘을 준다는 의미
	rigid.AddForce(Vector3.up * jumpPower,ForceMode.Impulse);

Unity 기능 살펴보기

1.InputManager 제어하기

현재는 InputSystem을 사용하여 프로젝트를 개발한다는데 기회가되면 InputSystem을 사용해봐야겠다.

InputSystem을 사용하면 크로스플랫폼 개발에 큰 도움을 준다.

 

2.테스트 중 캐릭터의 점프가 우주에서 하는것처럼 느껴질때 대처

오브젝트의 RigidBody 를 건드는것이 아닌 ProjectSetting의 중력 값을 조절하면된다.

y값이 작아질수록 중력이 쎄진다.

 

3.캐릭터가 벽과 충돌할때 벽에 끼이면서 느리게 떨어지는거 대처

벽을 뚫는거는 잠시 무시하자 , 집중해야되는 건 벽에서 내려올때 끼이는것이 문제

해당 문제를 해결하기 위해 3가지 방법을 적용해야한다..

3-1. Rigidbody의 Collision Detection > Continuos는 Static과 충돌할 때 효과적

3-1-1. transform 이동은 가끔 물리충돌을 무시하는 경우가 발생 함 ,

Rigidbody 컴포넌트에서 CollisonDetection을 Continuos로 변경하게 되면 계속 충돌처리를 탐색하게 된다.

이렇게 되면 Cpu 처리비용을 더 먹어 최적화는 떨어지지만 정확도는 올라가기 때문이다.


3-2.서로 두 오브젝트가 충돌할때 유니티가 효과적으로 계산하기 위해서는 두 오브젝트다 Rigidbody를 가지고 있어야함

3-2-1.고정 오브젝트인 경우 is Kinematic을 체크하고 Use Gravity를 체크해제 해주면됨 


3.점프하면서 벽에 비벼지며 느리게 떨어지는 것은 Physic Material을 이용하여 
마찰력이 없는 벽을 만들어 Collider에 적용

Physic Material은 물리가 적용되는 재질로 Collider에 적용된다.

 

이럼 이제 벽에 점프해서 충돌할때 매끄럽게 내려오는 모습을 볼 수 있다.

 

 

4.Animater의 Parameter 결정 조건

현재 프로젝트에 적용되고있는 Player - MeshObject의 Animator

Roop Animation들은 보통 bool 파라미터를 사용한다.

일회성 애니메이션(점프,회피)은 Trigger 파라미터를 사용한다.

Trigger 파라미터는 doXXX로 작성 하였고 스크립트에서 On을 하면 애니메이션이 재생되고

애니메이션이 끝난 후 Off하게 되는 파라미터이다.

 

5.3D에 오브젝트(부모,자식)배치하기 - 하이어라키(계층 구조의 이해)

오브젝트의 위치는 그대로고 자식오브젝트인 MeshObject만 조절하여 땅에서 올리고 회전시킨 모습

부모오브젝트는 (0,0,0) 위치에 배치하고 MeshObject의 위치(0,1.7,0)만 조절하여 땅위로 올린 모습이다.

플레이어 캐릭터의 하이어라키 구조

모든 프로젝트가 이런건 아니지만 3D에서는 Sprite가 아닌 Mesh들을 가지고 3D 오브젝트를 관리하기에

내 나름대로 정리하는게 좋을것 같아서 정리 하였다.

 

1.Player의 최상위 부모에 Rigid,Collider를 넣어서 물리현상 구현 및 Player 스크립트 동작을 하게 하였다.

2.Mesh Object는 캐릭터의 모습을 보여주는 기능이기에 따로 자식으로 뺸뒤 Animator 컴포넌트를 추가하여 캐릭터의 모습에 애니메이션을 추가하는 기능까지 부여하였고

3.기능적인건은 따로 자식오브젝트로 뺼 수도 잇고(무기를 든다거나,스킬을 가지고 있다거나 등등)

 

즉, 프로젝트 설계에 따라 부모 -자식 및 기능 적으로 분리를 잘 해야 프로젝트가 수월해진다.

 

 

6. Object에 Light 추가하기 + Particle

아이템에 Light , Particle 빈 오브젝트를 만든뒤 각각 Light , Particle System컴포넌트를 추가하였다.

Transforn . y 값 조절 : 위치를 조절하여 빛의 위치 조정

Light - Intensity : 빛의 세기 조절

Light - Range : 빛의 범위 조절

마지막 움짤을 보면 아이템 밑에 부위에도 빛이 비치는 장면이 있다.

 

*Particle System 간단 설명

Particle System의 Particle 카테고리
위에 설명한 기능들을 적용하는 모습

 

Particle System - Renderer 설정

Material을 변경하여 파티클 이미지를 변경 할 수 있다.

Particle System - Emission 기능

Emission(방출)은 파티클의 갯수에 영향을 준다. Time의 숫자가 커질수록 파티클의 갯수가 증가한다.

 

 

ParticleSystem-Shape 기능

Shape 기능은 파티클이 어떤 방식으로 뻗어 나갈건지 결정하는 기능이다. Corn 모양일때 원뿔 모양에서 파티클이 뿜어져 나왔고 Donut일때는 사방으로 퍼져 나오는 모습을 볼 수있으며 퍼져나오는 근원의 위치,각도,크기를 조절 할 수 있다.

 

ParticleSystem-Color over LifeTime 기능

Color over LifeTime 은 파티클의 색상을 결정하는 기능이다. 해당 이미지를 보면 이해가 될것이다. 

색상 변경을 하여 위로 올라갈수록 색이 변경되는 모습을 볼 수있다.
ParticleSystem-Size over LifeTime 기능

Size over LifeTime 은 파티클이 살아있는동안 크기를 변화하는 기능이다.

Unity에서 제공하는 프리셋이 있어 그걸 사용해도 된다. 위의 이미지 처럼 위로 올라갈수록 파티클의 크기가 커지는 모습을 볼 수있다.

 

아이템 연출 완성

 


2024-09-04(수)

https://www.youtube.com/watch?v=WkMM7Uu2AoA&list=PLO-mt5Iu5TeYkrBzWKuTCl6IUm_bA6BKy 

: 에셋 다운 + 플레이어 이동 및 카메라 설정

https://www.youtube.com/watch?v=eZ8Dm809j4c 

: 점프 / 회피 로직 구현

https://www.youtube.com/watch?v=u2DLOay5oO8  

: 아이템 생성 및 파티클 / 라이트 컴포넌트 조작 (12:20초까지 수강)

 

 

 

 

 

 

Cinemachine(씨네머신)을 이용한 카메라 컨트롤

예전에는 카메라를 관리/제어하기 위해 직접 코드를 작성햇지만 , 

씨네머신은 Scene에 여러 카메라를 관리하고 쉽게 카메라 법칙을 추가 할수 있는 Unity에서 지원하는 강력한 패키지 이다.

 

*오브젝트를 따라가는 간단한 카메라 설정하기 - Cinemachine

MainCamera 오브젝트에 CinemachineBrain 컴포넌트를 추가한다.

 

[GameObject] - [Cinemachin] - [VirtualCamera]를 통해 가상 카메라 오브젝트를 생성한다.

 

Follow에 오브젝트(노란상자)를 등록하면 해당 오브젝트를 계속 따라다니면서 촬영하게 된다.
Look At에 오브젝트(노란상자)를 따라가진 않지만 계속 오브젝트 방향으로 촬영을 한다.
Follow : 노란상자 오브젝트 / Look At : 파란색 긴 기둥 인경우 이런 촬영도 가능하다.

 


유니티 3D 강의를 듣는 도중 씨네머신 얘기를 해서 복습할겸 적어보았다.

씨네머신은 나도 잘 다룰 줄 모르는데 이번 강의 기회에 제대로 사용하는 법을 익혀야겠다.

https://luckygg.tistory.com/181

 

[Design Pattern] 옵저버 패턴(Observer Pattern) 이야기 #1 (예제 포함)

옵저버 패턴(Observer Pattern) 옵저버 패턴은 관찰자 패턴이라고도 합니다. 일대다 관계를 이루고 있으며, 상태가 업데이트되면 모든 옵저버들에게 정보를 갱신할 수 있도록 하는 것을 의미합니다.

luckygg.tistory.com

해당 블로그 글을 보며 정리하였습니다.

 

Observer Pattern(옵저버패턴)

  • 디자인패턴 중 하나
  •  한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들에게 정보가 자동으로 갱신되는 방식으로, 일대다(one-to-many) 의존성을 정의합니다
  • Subject(관찰대상,주제자)업데이트 되면 Observer(관찰자)들에게 정보를 갱신 할 수 있도록 함
  • ex)Subject(유튜브채널,신문사) / Observer(구독자들)

Observer(관찰자) 특징

  • 언제든지 새로 추가 되거나 제거 될수 있어야 함
  • 추가 / 삭제가 되더라도 Subject에 영향을 주지 않음 :: 해당 관계를 느슨한 결합(Loose coupling)이라고 함
  • C++ 에서 구현시 Observer는 특정 interface를 구현해야만 함
  • 해당 interface에서는 Observer는 Update() 기능을 구현함

Subject(주제자) 특징

  • Observer에 대한 정보가 없음 : Observer가 무슨 동작을 하는지 모름
  • C++ 에서 구현시 Subject특정 interface를 구현해야만 함
  • 해당 interface에서는 상속받는 Subject Class의 등록,해제,갱신을 구현 및 기타 함수를 구현함.

C++ 구현시의 UML로 표현한 Observer 패턴 구조

 


C#에서는 따로 옵저버 패턴을 구현할 필요가 없다.

이 기능을 Deligate 와 Event 함수가 대신하기 떄문이다.

 

해당 출처를 참고하면 더욱 도움이 된다.

https://unity.com/kr/how-to/create-modular-and-maintainable-code-observer-pattern

 

옵저버 패턴으로 유지 관리가 가능한 모듈식 코드 만들기

이 페이지에서는 관찰자 패턴과 이 패턴이 서로 상호작용하는 객체 간의 느슨한 결합 원리를 지원하는 데 어떻게 도움이 되는지 설명합니다.

unity.com

 

 

 

유니티 5주차 - 하이퍼캐주얼 게임 개발에서 터치패드를 구현할떄 사용 하였다.
해당 게임에서 구현한 옵저버 패턴

나는 옵저버패턴을 이런식으로 구현해서 사용했지만 , 실제로는 이렇게 구현하면 안된다.

일대다관계로 구성이 되야하지만 현재 일대일 관계로 2개를 구현하였다.

다음에는 구현 시 주의하면 만들어봐야겠다.

 


C# - Delegate Vs Envet ?

C++ 의 함수포인터C#으로 변환한게 Delegate이다. 

Delegate멀티캐스팅 기능으로 하나의 델리게이트로 여러 인스턴스(객체)가 여러 메서드(함수)를 호출 할수 있도록 구성이 가능하다.

 

하지만 이렇게되면 객체지향의 4대원칙 중 보안성이 지양되기 때문에 Delegate가 아닌 Event 함수가 주로 사용됩니다.

Event함수는 Delegate기반으로 되있지만 엑세스 제한으로 외부에서 이벤트를 추가(+=)/ 제거(-=)만 가능하며 이벤트 핸들러에 등록된 함수가 호출되지 않는다.

 

즉, Event함수의 함수호출은 내부클래스에서만 호출이 된다.

 

특징 Delegate Event
역할 함수포인터로 사용 델리게이트에 대한 접근을 제한하고 이벤트 모델 제공
엑세스 제어 어디서든 호출 가능 선언된 클래스 외부에서는 호출불가
사용 목적 일반적인 콜백이나 함수 참조 관리 안전한 이벤트 발생 및 처리
기본 엑세스 public 특별히 제한된 엑세스
(add/Remove만 허용)
멀티캐스트 지원 가능 가능

 


<이 부분은 개인적으로 생각한 것을 적은 부분입니다.  틀린 내용이 있으시면 댓글 부탁드립니다 >

 

왜 하나의 클래스에 Delegate와 Event를 동시에 쓰나요?

 

Delegate함수 포인터라는 자료형의 변수를 선언하는 것과 유사하다.

그리고 Event는 Delegate에서 선언한 변수를 자료형으로 삼고 사용한다.

  public delegate void TouchPadMoveHandler(); // delegate 키워드를 사용하여 함수포인터 변수 선언
  public static event TouchPadMoveHandler OnTouchLeftPad; //선언한 변수를 자료형으로 event 함수 선언

'내일배움캠프_Unity_6기 > TIL(Today I Learend)' 카테고리의 다른 글

TIL : 2024-09-04(수)  (0) 2024.09.04
TIL : 2024-09-03(화)  (0) 2024.09.03
TIL : 2024-08-29(목)  (0) 2024.08.29
TIL : 2024-08-28(수)  (0) 2024.08.28
TIL : 2024-08-27(화)  (0) 2024.08.27

인기게임 역기획 하기

 

역기획 대상 게임 : 로스트아크 

더보기
  1. 이 게임의 매력 포인트는 무엇인가요?
  • 다양한 플레이어블 캐릭터 직업군
  • 로스트아크는 26개의 직업군'직업 각인'이라는 시스템으로 한개의 직업군으로 두가지의 전투 스타일을 경험할 수 있습니다.
  • 보스 레이드의 패턴 파훼 및 클리어 성취감이를 파훼하고 클리어하는 과정에서 높은 성취감을 제공합니다. 특히, 플레이어들은 팀원들과 협력하여
  • 다양한 기믹(협동 카운터,보스 전멸 기믹 넘기기 등등)을 파훼할 수 있고 보스의 공격 패턴을 분석(저스트가드 및 카운터하여 딜타임 확보)하며 숙련될 수록 더욱 재미를 느끼게 하였습니다.
  • 보스는 고유의 공격 패턴난이도를 가지고 있으며,
  • 수평 방향의 내실 및 다양한 수집 콘텐츠
  • 전투 및 레이드만 아닌 섬 탐험, 항해, 생활 콘텐츠(낚시, 채광 등), 모험의 서, 카드 수집 등의 컨텐츠로 다양한 볼거리 와 재미를 제공합니다.

2. 이 게임을 사람들이 좋아하는 이유

  • 풍부한 콘텐츠와 스토리의 몰입도
  • 이 게임은 전투/수집 콘텐츠에서도 다양하고 깊이 있는 재미를 유저들이 느낄 수 있으며 그 뿐만 아니라 위의 컨텐츠를 즐기면서 메인스토리 및 서브스토리 전부 유저들이 몰입하여 감상 할 수 있는 점을 사람들이 좋아하는 이유라고 생각합니다.

3.게임 개요

3-1. 이름 : 로스트아크

3-2. 장르 : MMORPG

3-3. 제작사 : 스마일게이트RPG,트라이포드 스튜디오

3-4. 출시일 : 2018년 11월 7일(정식 출시 시즌1 2019년 12월)

3-5. 엔진 : 언리얼 엔진3

4.시스템 개요

4-1. 보스레이드 : 군단장 레이드 및 카제로스 레이드의 지원 시스템

4-2. 지원 시스템(에스더스킬,연합군스킬)은 8인 이상 레이드 컨텐츠에서 공대장이 사용하는 지원 스킬로 전투 시에 다양하게 이로운 점을 줘서 선택했습니다.

기능은 스토리를 진행하면서 만났던 NPC들을 호출하여 전투를 유리하게 이끌고 가는 시스템 입니다.

전투 중 게이지를 모아서 원하는 타이밍에 공대장이 해당 버튼을 누르면 NPC가 등장하여 전투를 유리하게 해주고 퇴장합니다.

사용 예시)

1.보스를 무력화 하여 딜타임을 만

2.전멸 기믹을 막아서 더욱 보스레이드를 수월하게 진

3. 특수한 타이밍에 사용해서 새로운 연출(보스와 NPC의 상호작용)로 유리한 전투를 극대화 함

 

해당 이벤트를 통해 아무리 인기 게임이여도 사람마다 생각하는 점이 달라 재미있었고 다르게 생각 할수 있었던것 같다.

5주차 숙제 게임개발

 

Run & Jump - 프로토타입 설명

1.마우스를 누르면 캐릭터 머리 위에 파워 게이지가 표시됨

2.마우스를 놓으면 놓은 방향으로 캐릭터가 빙글빙글 돌아가며 발판들을 넘어감

3.이미 한번 착지한 발판은 점수 x , 발판을 처음 밟을때만 점수 증가

 

오늘 구현 한것

1. 블럭 사이에 있을때 점프로 가시 통과할 수 있게 구현

2. 블럭 끼리 겹치지 않게 구현 - BlockSpanwer에서 생성 로직 변경 필요

3. LV에 따른 스테이지 난이도 변경

4. 블럭에 빠지는 함정 만들기(함정 특징: 땅바닥인척 하지만 갑자기 내려감)

4-1. Player 점프 로직 변경(떨어지는 블럭 위에 있을때 점프 가능하게)

 

구현 목표

1.옆으로 흐르는 백그라운드 만들기

2.Title Scene 만들기(마스크 트랜지션 사용해서 화면 옮겨보기)

3.파티클 및 연출 효과 넣어 시각적으로 좋게 만들기

4.사운드 추가하기


1. 블럭 사이에 있을때 점프로 가시 통과할 수 있게 구현

 

각 블럭의 높이를 줄여 공간을 확보 하였고 , 가시의 갯수가 많아 게임이 너무 어려워 1개로 듬성듬성 배치하기로 함

 

2. 블럭 끼리 겹치지 않게 구현 - BlockSpanwer에서 생성 로직 변경 필요

코드를 따로 변경하진 않았고, 블럭의 속도를 전부 똑같이 통일 하였다.

전부 똑같이 변경한 이유는 게임이 [점프킹] 같은 점프 시스템을 가져왔다보니

블럭들의 속도가 종류에 상관없이 일정해야  게임의 난이도가 좀 나아질것 같다.

 

3. LV에 따른 스테이지 난이도 변경

BlockSpanwer.cs

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

public enum defineNum
{
    Blokc_End_posX = -17, //해당 x좌표에 닿으면 pivot이 닿으면 사라지는 좌표 
}

public class BlockSpawner : MonoBehaviour
{
    [SerializeField] List<Block> block_list;
    [SerializeField] float blockSummonCoolTime = 2.0f;

    [SerializeField] float startPosX = 17.0f; //생성 시작되는 위치 x좌표
    [SerializeField] float startPosY_Min = -5.0f;
    [SerializeField] float startPosY_Max = 2.0f;

    Coroutine coroutineBlockSpanw;
    [SerializeField] List<float> blockSpawnPosY;

    [SerializeField] bool isstop = false;

    int isSpawnBlockIndex = -1;

    public void BlockSpawnerON()
    {
        if (!isstop)
        {
            if (coroutineBlockSpanw == null)
            {
                coroutineBlockSpanw = StartCoroutine(BlockSpanw());
            }
            else
            {
                StopCoroutine(coroutineBlockSpanw);
                coroutineBlockSpanw = StartCoroutine(BlockSpanw());
            }
        }
    }

    public void BlockSpawnerOFF()
    {
        if (coroutineBlockSpanw != null)
        {
            StopCoroutine(coroutineBlockSpanw);
        }
    }

    IEnumerator BlockSpanw()
    {
        float LevelSuummonCoolTime;

        while (true)
        {
            MakeBlock();

            //현재 레벨의 10% 만큼 블럭 생성주기 증가 -> 블럭이 늦게 등장함
            LevelSuummonCoolTime = blockSummonCoolTime + ( (GameManager.Instance.CurLevel - 1) *  0.1f);
            
            yield return  new WaitForSeconds(LevelSuummonCoolTime);
        }
    }

   private void MakeBlock()
    {
        //블록 생성확률이 100% 에서 75%까지 떨어짐
        //생성확률은 6레벨 이하까지만 적용 그 이후로는 50% 동일
        int curStageLv = GameManager.Instance.CurLevel;
        int SuumonPercent = curStageLv >= 5 ? 5 : (curStageLv - 1);

        if (SuumonPercent <= Random.Range(0, 20))
        {

            int type = Random.Range(0, block_list.Count);

            Block block = Instantiate(block_list[type]);
            block.gameObject.SetActive(false);

            float x = startPosX;
            //y 범위 : -4.5 ,-1.5,1.5, 4.5
        
            //한번 생겼던 위치에 다시 생기지 않는 함수
            int y;
            do
            {
                y = Random.Range(0, blockSpawnPosY.Count);
            }
            while (isSpawnBlockIndex == y);
            isSpawnBlockIndex = y;


            block.Block_pos = new Vector3(x, blockSpawnPosY[y], 0);
            //레벨 수치의 10% 만큼 모든 블럭의 speed 증가
            if (curStageLv > 1) block.Speed += (curStageLv * 0.1f);
            block.gameObject.SetActive(true);
        }
    }
}

블럭 생성시 게임매니저에서 현재 스테이지 레벨을 호출 하여 블록 생성 확률,블록 생성 주기,블록 이동 속도 수치를 조절하게 함.

4. 블럭에 빠지는 함정 만들기(함정 특징: 땅바닥인척 하지만 갑자기 내려감)

 

1.좌우로 이동하는 블록 추가

 

BlockGroup.cs

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

public class BlockGroup : Block
{
    [SerializeField] protected List<GameObject> blocks;

    private void Update()
    {
        MoveBlock();
    }
}

MovePattern.cs

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

public class MovePattern : MonoBehaviour
{
    [SerializeField] float avoidSpeed = 2.0f;

    int dir = 1;
    private Vector3 originPos;
    private float moveRange = 1.0f;

    private void Start()
    {
    	//처음 위치 좌표 저장
        originPos = transform.localPosition;
    }

    private void Update()
    {
    	//이동거리(MoveRange) 이상 이동한경우 방향값(dir) 변경
        if(Mathf.Abs(transform.localPosition.x - originPos.x) >= moveRange) dir *= -1;
         
        transform.localPosition += dir * ((Vector3.right  * avoidSpeed)) * Time.deltaTime;
    }
}

좌,우로 왔다 갔다 하는 패턴을 만들때 왼쪽/오른쪽 좌표로 설정해도 되지만

조건문을 이동거리(MoveRange)로 사용하면 좌표 값을 건드리지 않고 이동거리로 제어가 가능해져서 유용하다.

2.밟으면 떨어지는 블록 추가

DropPattern.cs

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

public class DropPattern : MonoBehaviour
{
    [SerializeField] float DropSpeed = 0.5f;

    private void OnCollisionEnter2D(Collision2D collision)
    {
        if(collision.gameObject.CompareTag("Player"))
        {
            StartCoroutine(Drop());
        }
    }

    IEnumerator Drop()
    {
        while(transform.position.y >= -12f)
        {
            transform.position += Vector3.down * DropSpeed * Time.deltaTime;
            yield return null;
        }
    }

}

 

위에 블럭들의 콜리더 분리가 필요해서 부모 오브젝트를 만들고 자식으로 3개의 블럭을 추가하였다.

각 블럭마다 콜리더를 추가하였고 기믹이 필요한곳에만 스크립트(MovePattern.cs , DropPattern.cs)를 넣어서 사용 하였고 3개의 블럭은 동시에 같은속도로 같이 이동해야 되기때문에

새 스크립트인 BlockGroup 스크립트를 만들고 Block을 부모로 상속하여 이동하는 함수를 사용하였다.

 

4-1. Player 점프 로직 변경(떨어지는 블럭 위에 있을때 점프 가능하게)

떨어지는 블럭에 캐릭터가 서있으면 OnColliderEnter() 메서드가 자꾸 호출하여서 점프가 안되는 버그가 있었다.

Collider 함수 안에 점프상태를 초기화하는 내용이 있음

대처방법으로 콜리더가 호출 되더라도 점프 상태인 경우에만 점프상태를 초기화 할 수 있게 로직을 변경하였음 

 

떨어지지만 게이지는 초기화 되지 않기 때문에 점프가 가능해짐

 

Player.cs

    private void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.CompareTag("Block"))
        {
            // 캐릭터가 점프 + 점프중 이라는 상태를 만족해야만 멈춤
            if (isJump && isJumping)
            {
                PlayerStop();
                anim.SetBool("isJump", false);
            }
        }

        if (collision.gameObject.CompareTag("Spike"))
        {
            Debug.Log("Player 가시 닿음");
            isdead = true;
            anim.SetBool("isDead", true);
            PlayerStop();
            GameManager.Instance.PlayerDie();
        }
    }

 

+ Recent posts