2024.10.16 - [게임만들기/UNITY] - [UNITY] 첫 3D 팽이 게임 만들기 - 각저항 분석 후 팽이 '잘'돌리기

0. 개발 목표

1) 필드에 벽을 세우고 팽이가 벽충돌시의 물리 적용하기

  • 기존 개발 방식은 Raycast의 거리가 땅을 검사하고 거리가 줄어드면 각 저항을 증가시켜 팽이를 멈춤
  • 벽과 땅을 구분하고 벽에는 Bounce + 이동방향 변경, 땅에는 각저항 증가

2) Bounce  구성하기

  • 벽에 팽이 Collider가 충돌하면 OnCollisionEnter()를 통해 진행방향 벡터 변경

1. 테스트 환경 구성

1) 기존 필드에 팽이 대신 테스트용 오브젝트 추가

 기존 팽이는 비활성화하고 3D 오브젝트 구에 RigidBody와 Wall, Ground에 Tag 추가 카메라 Follow 설정 변경

벽은 Kinematic를 활성화하고 충돌과 관련한 모든 오브젝트에 Collision Detection을 Continuous로 변경하여 오브젝트가 프레임 차이시간에 뚫고 나가는 것을 방지.

테스트 환경 구성

2)  ColliderTest 스크립트 임시 추가

  1. 상하좌우 입력받고 벽에 붙을때 Log 출력 추후 다른 오브젝트 충돌과도 고려하여 switch 문으로 작성하고 오브젝트의 Tag를 비교하여 각 오브젝트에 대한 함수 실행
        private void OnCollisionEnter(Collision other)
        {
            switch(other.gameObject.tag){
                case "Floor":
                    Debug.Log("땅");
                break;
    
                case "Wall":
                    Debug.Log("벽");
                    break;
                }
        }

    'Hostria'에서 업로드한 동영상

     



  2. 벽과 충돌시 입력된 벡터의 반사 벡터 방향으로 충돌 오브젝트에 AddForce() 하는 함수 구현.
    private void OnCollisionEnter(Collision other)
    {
        switch(other.gameObject.tag){
            case "Floor":
                Debug.Log("땅");
            break;

            case "Wall":
                Vector3 normal =other.contacts[0].normal;
                Debug.Log("벽");
                DoBounce(normal);
                break;
            }
    }
    void DoBounce(Vector3 normal)
    {
        Debug.Log("Dobounce = " + Vector3.Reflect(inputVec,normal) );
        rigid.AddForce(Vector3.Reflect(inputVec,normal)*speed, ForceMode.VelocityChange);

    }

 

Vector에서 Reflect 매게변수는 입사 벡터, 법선벡터로 벽과 충돌시점의 포인트의 법선벡터 인 normal을 이용하여 DoBounce를 수행한다. 보기 쉽게 임시적으로 반사벡터에 100f 의 힘을 추가했다.

using System.Collections;
using System.Collections.Generic;
using UnityEditor.Callbacks;
using UnityEngine;

public class ColliderTester : MonoBehaviour
{
    public float speed = 10f;
    public float transX;
    public float transY;
    Vector3 inputVec;
    Rigidbody rigid;

    private void Awake() 
    {
        rigid = GetComponent<Rigidbody>();    
    }
    private void Update() 
    {
        transX = Input.GetAxisRaw("Horizontal");
        transY = Input.GetAxisRaw("Vertical");
        inputVec = new Vector3(transX, 0, transY).normalized  * Time.deltaTime * speed ;

    }
    private void FixedUpdate() 
    {
        transform.Translate(inputVec);

    }
    private void OnCollisionEnter(Collision other) {
        switch(other.gameObject.tag){
            case "Floor":
                Debug.Log("땅");
            break;

            case "Wall":
                Debug.Log("벽");
                Vector3 vector3 =other.contacts[0].normal;
                DoBounce(vector3);
                break;
            }
    }
    void DoBounce(Vector3 inNor){
        Debug.Log("Dobounce = " + Vector3.Reflect(inputVec,inNor) );
        rigid.AddForce(Vector3.Reflect(inputVec,inNor)*100,ForceMode.VelocityChange);

    }
}

 

 

 

 

테스트를 하고나니 오래 돌게할려고 꺼둔 중력이 문제다.

벽에 충돌하는 순간 팽이에 Y 값이 충돌한 벡터 만큼 변경되어 중구난방으로 튀어버리는 것.

모양이 큐브이거나 구체인 경우 덜컹거리는(?) 느낌의 조정으로 다시 회전시키면 괜찮은데 팽이가 진행방향과 다른 방향의 기울기로 변경되고 중력이 꺼지면서 매우 이상하게 회전했다. 다음에는 해당 내용을 좀 연구해서 충돌을 추가하려 한다.

0.AddTorque()를 이용해서 팽이 Rotation 구현하기

2024.10.15 - [UNITY] - [UNITY] 첫 3D 팽이 게임 만들기 - 팽이 프로토타입 만들기

 

[UNITY] 첫 3D 팽이 게임 만들기 - 팽이 프로토타입 만들기

유니티에서 게임 오브젝트를 만들면서 기본적으로 제공되는 도형들이 많이 불편했다. Assets을 찾아봤지만 교통 꼬깔형태의 Cone이 대부분이고 폴리곤 형태에서 원뿔에 가까운 형태는 직접 모델

hostria.tistory.com

어제 글을 작성하고 인스펙터 창에 이것저것 실험해 보았다.

회전력을 크게 주어도 생각한 만큼 회전이 나오지 않았다.

이에 최대한 고정프레임에서 가장 많이 그리고 빠르게 회전하는 팽이를 만들기 위해 물리 연산에 변수들을 정리 분석해 보았다.

 

1. AddTorque() 분석을 위한 임시 코드 수정

using System.Collections;
using System.Collections.Generic;
using System.Numerics;
using UnityEngine;
using Vector3 = UnityEngine.Vector3;

public class SpinController : MonoBehaviour
{
    [SerializeField] private float initRotateSpeed; // 초기회전력
    [SerializeField] private float speed;   //이동속도
    private Vector3 inputVec; //상대 이동벡터

    [SerializeField]private float speedDamper; //속도 감소률
    [SerializeField]private float rotateSpeed; //회전력
    private Rigidbody rb;
    
    private void Awake() {
        Application.targetFrameRate = 60; //고정프레임 60FPS 
        rb = GetComponent<Rigidbody>();
        initRotateSpeed = 1f;
        speed = initRotateSpeed*0f;
        speedDamper = 0.99f;
        rotateSpeed = initRotateSpeed;

    }

    private void Update()
    {
        //float inputX = Input.GetAxis("Horizontal");
        
        //inputVec = new Vector3(inputX, 0, 0) * speed *Time.deltaTime;

    }
    void FixedUpdate()
    {
        //transform.Translate(inputVec, Space.World);
        rb.AddTorque(transform.up * initRotateSpeed * Time.fixedDeltaTime);
    }

}

이번 분석을 위해 회전과 관련 없는 코드는 삭제 수정했으며, 고정 프레임을 추가하였다.

고정 프레임을 작성한 이유는 매번 돌릴 때마다 200~350 FPS 차이의 변동으로 회전과 이동이 변했기 때문이다.

 

 

[결과] 금방 땅에 쓰러지고 남은 회전으로 굴러다니면서 원뿔이 탈출함

하도 바닥에서 뺑뺑 돌기만 해서 바닥에서 얼마나 굴렀는지 확인하는 스크립트를 추가했다.

실험. 초기회전력 100f

 

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

public class CheckerLogger : MonoBehaviour
{

    private void OnTriggerEnter(Collider other) {
        if(other.CompareTag("Floor")){
                    Debug.Log("쓰러짐");
            return;
        }

    }
}

 

2.RigidBody의 AddTorque(Vector3 vector, ForceMode force);

파라미터 vector = 회전축과 회전력,

파라미터 force =  RigidBody의 1. 질량(M), 2. 프레임당 시간(DT), 3. 토크(Tor)의 계산방식을 결정한다.

force의 변수

ForceMode.Force = Tor*DT/M

ForceMode.Acceleration = Tor * DT

ForceMode.Impulse =Tor/M

ForceMode.VelocityChange = T

 

ForceMode.Force

 

 

RigidBody의 Mass(질량), Drag(저항), Angular Drag(각 저항)은 모두 물리연산에 영향을 준다. 

 

이번 팽이 돌리기에 활용 변수는 각저항을 이용하고자 한다.

 

값을 변경하면서 실험해 본결과 의미 있는 내용 몇 가지를 추려보았다.

 

[실험 1] M:D:A  = 1 : 0 : 0.05

 

[결과 1]

약 2바퀴 회전 후 쓰러짐, 이후 남은 회전에 의해 밖으로 탈출.

쓰러지는 것을 체크할 필요가 있음.

 

[실험 2] M:D:A = 1 : 0 : ~50

팽이가 멈출 때까지 각저항을 증가시킴.

 

[결과 2]

Angular Drag = 50 될 때부터 회전 멈춤.

각저항을 점진적으로 증가시켜서 팽이가 쓰러지고 완전히 멈추게 해야 함.

 

착지하자마자 각 저항이 오른다면 회전력 유지 못함.

따라서

  1. rotationSpeed와 initRotateSpeed를 이용해서 회전 유지 시간을 확보 중력을 끄고 키면서 오래 돌아가는 것처럼 보이게 설정.
  2. Raycast를 이용해 바닥을 체크하고 쓰러질 때의 거리를 측정하여 각저항과 중력을 통제. 
using System.Collections;
using System.Collections.Generic;
using System.Numerics;
using JetBrains.Annotations;
using Unity.VisualScripting;
using UnityEngine;
using Vector3 = UnityEngine.Vector3;

public class SpinController : MonoBehaviour
{
    [SerializeField] private float initRotateSpeed; // 초기회전력
    [SerializeField] private float speed;   //이동속도
    private Vector3 inputVec; //상대 이동벡터

    [SerializeField]private float speedDamper; //속도 감소률
    [SerializeField]private float rotateSpeed; //회전력
    private Rigidbody rb;
    private RaycastHit hit;
    private bool isGrounded;
    private bool isStoped;

    private void Awake() {
        Application.targetFrameRate = 60; //고정프레임 60FPS 
        rb = GetComponent<Rigidbody>();
        initRotateSpeed = 100f;
        speed = initRotateSpeed * 0.1f;
        speedDamper = 0.99f;
        rotateSpeed = initRotateSpeed;
        isGrounded = false;
        isStoped = false;
    }

    private void Update()
    {
        StateCheck();
        
        float inputX = Input.GetAxis("Horizontal");
        
        inputVec = new Vector3(inputX, 0, 1) * speed * Time.deltaTime;
    }
    void FixedUpdate()
    {
        transform.Translate(inputVec, Space.World);
        rb.AddTorque(new Vector3 (0,100000,0) * rotateSpeed, ForceMode.VelocityChange);

    }

    public void StateCheck(){
        if(isGrounded){
            if (Physics.Raycast(rb.transform.position, transform.right, out hit))            {
                Debug.DrawRay(rb.transform.position, transform.right * hit.distance, Color.red);
                if(hit.distance < 1f && rb.angularDrag < 50){
                    Debug.Log("쓰러지이인다");
                    rb.angularDrag += 0.65f;
                }
                if(rb.angularDrag >=50){
                    Debug.Log("멈춤");
                    isGrounded = false;
                    isStoped = true;
                }
             else
                {
                Debug.DrawRay(rb.transform.position, -transform.up * 1000f, Color.red);
                }
            }
        }
      
        if(!isStoped){
            if (Physics.Raycast(rb.transform.position, -transform.up, out hit))
            {
                Debug.DrawRay(rb.transform.position, -transform.up * hit.distance, Color.red);
                if(hit.distance < 0.25f){
                    Debug.Log("착지");
                    isGrounded = true; 
                    rb.useGravity = false;
                }
            }
            else
            {
                Debug.DrawRay(rb.transform.position, transform.right * 1000f, Color.red);
            }
            if(rotateSpeed < 1){
                rb.useGravity = true;
            }else{
                rotateSpeed *= speedDamper;
                speed *= speedDamper;
            }
        }
        else {
            speed = 0;
            rotateSpeed =0;
        }

    }
}

 

실행 결과 어제보다는 쪼금 더 잘 도는 거 같다.

 

0.유니티 프로빌더와 원뿔같은 Cone 만들기

유니티에서 게임 오브젝트를 만들면서 기본적으로 제공되는 도형들이 많이 불편했다.

 

Assets을 찾아봤지만 교통 꼬깔형태의 Cone이 대부분이고 폴리곤 형태에서 원뿔에 가까운 형태는 직접 모델링해야 한다.

 

Blender 써볼까? 했지만 우선 '3D 게임 공부!' 목적이므로 프로빌더 패키지를 활용하기로 했다.

 

1. 프로빌더 설치.

Unity에서 제공하는 3D 모델링 패키지로 Package Manager 에서 바로 설치할 수 있다.

 

[Tools] -> [ProBuilder] -> [ProBuilderWindow] 로 바로 윈도우를 추가해 주었다.

프로빌더 설치후 윈도우 추가

 

2.원뿔 만들기

New Shape를 선택해서 씬창에 생긴 도구를 이용해 Cone을 만들어 보았다.

Create Shape와 Cone 선택

 

Cone을 선택하고 [CTRL] + [Shift] 버튼을 이용해서 원뿔에 가까운 오브젝트를 생성해 주었다. 딸깍!x3

Side Count는 64로 설정하였다.

Cone 오브젝트 생성

팽이처럼 뒤집기 위해서 Transform을 수정하기 위해 오브젝트를 클릭했더니 중심이 아닌 피봇을 잡고 있었다. ProBuilder 창에서 [Center Pivot]을 클릭하여 축을 오브젝트 중앙으로 변경해 주었다.

피봇 설정전
피봇 설정 후

팽이를 돌리기 위해 땅과 색을 추가했다.

땅과 색 추가
생성한 GameObjects

 

3.RigidBody 와 Collider 컴포넌트

Cone은 Spinner로, Plane으로 만든 땅은 Ground로 이름을 변경해 주었다. 둘 다 ProBuilder로 생성하면서 Mesh Collider는 자동으로 추가되어 있다.

 

[Spinner]

Transform.position (0,1,0)

Mesh Collider - [Convex] 설정

RigidBody - 추가

 

[Ground] 

대충 Spinner 밑에 Z축으로 길게

 

4.스크립트 추가. SpinController.cs

구현하고자 하는 기능은 팽이가 회전하면서 이동하고 점점 힘을 잃어 바닥에 구르는 것.

using System.Collections;
using System.Collections.Generic;
using System.Numerics;
using UnityEngine;
using Vector3 = UnityEngine.Vector3;

public class SpinController : MonoBehaviour
{
    [SerializeField] private float rotateSpeed; // 회전력
    [SerializeField] private float speed;   //이동속도
    private Vector3 playerVec; //Player 절대 이동 벡터
    private Vector3 inputVec; //상대 이동벡터

    private float speedDamper; //속도 감소
    private Rigidbody rb;
    
    private void Awake() {
        rb = GetComponent<Rigidbody>();
        rotateSpeed = 10000;
        speed = 100;
        speedDamper = 0.999f;
    }
    void Start()
    {
        
    }
    private void Update() {
        float inputX = Input.GetAxis("Horizontal");
        float inputY = Input.GetAxis("Vertical");

        inputVec = new Vector3(inputX, 0, inputY)*Time.deltaTime;

        rotateSpeed *= speedDamper;
        speed *=speedDamper;
    }
    void FixedUpdate()
    {
        transform.Translate(inputVec*speed,Space.World);
        rb.AddTorque(new Vector3(0,1,0)*rotateSpeed*Time.fixedDeltaTime,0);
    }

}

 

Spinner에 스크립트 추가 후 테스트 해보았다.

잘 돈다.

이동도 구현했으나 뭔가 팽이스럽지 않아서 다음시간에 추가해보고자 한다.

+ Recent posts