[SKKU DT] 75일차 -교통 시뮬레이터(Traffic Simulator) 만들기

2024. 2. 20. 18:36SKKU DT

728x90
반응형

백터의 내적(dot)

Cube 생성 - 기본위치

sphere 생성 - (0, 0, -3)

 

 

TestScript 생성

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

public class TestScript : MonoBehaviour
{
    public TextMeshProUGUI textLabel;
    public Transform target;

    // Update is called once per frame
    void Update()
    {
        // 타겟 없이는 동작하지 않습니다. (실무에서도 꼭 주석을 달아준다.)
        if (target == null)
        {
            return;
        }
        // cube transform
        Vector3 lhs = transform.forward;
        // target 으로 향하는 젝터, 크기를 노멀라이즈해서 방향만 얻습니다.
        Vector3 rhs = (target.position - transform.position).normalized;
        // 내적을 구합니다. 최대 1, 최소 -1.
        float dot = Mathf.Clamp(Vector3.Dot(lhs, rhs), -1, 1);
        // 타겟 포지션으로부터의 역벡터를 구합니다.
        Vector3 lineVector = transform.InverseTransformPoint(target.position);
        // 레이를 그려봅니다.타겟으로 향하는 레이, 큐브의 forward를 나타내는 레이.
        Debug.DrawRay(transform.position, lineVector, Color.red);
        Debug.DrawRay(transform.position, transform.position, Color.cyan);
        // 텍스트로 내적의 값을 출력합니다.
        // .ToString("F1")은 소수점 한자리까지 표현한다는 뜻.
        textLabel.text = dot.ToString();
    }
}

 

 

실행시키면 아래와 같이 나온다.

 

 

게임뷰에서는 아래 화면처럼 나온다.

내적이 0보다 크면 나보다 앞, 내적이 0보다 작으면 나보다 뒤를 의미한다.

 

 


 

 

교통 시뮬레이션(Traffic Simulation) 만들기

강사님이 제공한 패키지 import (resource_first.unitypackage)

 

 

씬 하나 만들어서 차량 올려놓기,

 

 

기존에 있던 [Mesh Renderer] 등 컴포넌트 다 지우고 바퀴 4개에 각각 [Wheel Collider] 추가, 차체에는 [Rigidbody] 추가

 

 

차체 [Rigidbody] 설정은 아래와 같다.

 

 

[Wheel Collider] 설정 값은 아래와 같다.

 

*참고 [Wheel Collider] 프로퍼티

 

 

차체에 [Box Collider] 추가해서 [is Trigger] 켜기

 

 

빈 자식 오브젝트 2개 만들어서 [Box Collider] 추가, 크기는 조정해준다.

 

 


 

 

WheelDriveControl 스크립트 생성

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.IO;
[RequireComponent(typeof(Rigidbody))] //Rigidbody를 삭제할 수 없다. 누군가 실수로 Rigidbody를 뺄 수 없다.
public class WheelDriveControl : MonoBehaviour
{
    public enum DriveType
    {
        RearWheelDrive, //후륜구동
        FrontWheelDrive, //전륜구동
        AllWheelDrive //4륜구동
    }
    
    public enum SpeedUnitType
    {
        KMH,
        MPH
    }
    [Tooltip("차량에 적용되는 다운포스")]
    public float downForce = 100f;
    [Tooltip("바퀴의 최대 조향 각도")]
    public float maxAngle = 60f;
    [Tooltip("조향 각도각에 도달하는 속도(선형 보간)")]
    public float steeringLerp = 5f;
    [Tooltip("차량이 방향을 바꾸려고 할 때의 최대 속도")]
    public float steeringSpeedMax = 8f;
    [Tooltip("구동 바퀴에 적용되는 최대 토크(힘)")]
    public float maxTorque = 100f;
    [Tooltip("구동 바퀴에 적용되는 최대 브레이크 토크")]
    public float brakeTorque = 100000f;
    [Tooltip("속도 단위")]
    public SpeedUnitType unitType = SpeedUnitType.KMH;
    [Tooltip("최소 속도 - 주행 시 (정지 / 브레이크 제외) 단위는 선택한 속도 단위, 반드시 0보다 커야 합니다.")]
    public float minSpeed = 2f;
    [Tooltip("위에서 선택한 단위의 최대 속도")]
    public float maxSpeed = 10f;
    [Tooltip("바퀴는 휠 콜라이더의 자식으로 따로 붙여줍니다. 링크 필요.")]
    public GameObject leftWheelShape;
    public GameObject rightWheelShape;
    [Tooltip("바퀴에 애니메이션 효과를 적용할지의 여부")]
    public bool animateWheels = true;
    [Tooltip("차량의 구동 유형 : 후륜, 전륜, 4륜")]
    public DriveType driveType = DriveType.RearWheelDrive;

    private WheelCollider[] wheels;
    private float currentSteering = 0f;
    private Rigidbody rb;

    public void Init()
    {
        rb = GetComponent<Rigidbody>();
        wheels = GetComponentsInChildren<WheelCollider>(); //하위에 있는 컴포넌트 가져오기

        for(int i = 0; i < wheels.Length; i++)
        {
            var wheel = wheels[i];
            //필요할 때만 바퀴 모양을 만들 예정
            if(leftWheelShape != null && wheel.transform.localPosition.x < 0)
            {
                var wheelShape = Instantiate(leftWheelShape);
                wheelShape.transform.parent = wheel.transform;
            }
            else if(rightWheelShape != null && wheel.transform.localPosition.x > 0)
            {
                var wheelShape = Instantiate(rightWheelShape);
                wheelShape.transform.parent = wheel.transform;
            }
            wheel.ConfigureVehicleSubsteps(10, 1, 1); //초기 세팅 (시작할 때 얼마나 큰 힘이 필요한가, 얼마나 줄어들 것인가, )
        }
    }

    private void Awake()
    {
        Init();
    }
    private void OnEnable()
    {
        Init();
    }
    //현재 속도를 단위에 맞추기
    public float GetSpeedMS(float speed)
    {
        if(speed == 0f)
        {
            return 0f;
        }
        return unitType == SpeedUnitType.KMH ? speed / 3.6f : speed / 2.237f;
    }

    public float GetSpeedUnit(float speed)
    {
        return unitType == SpeedUnitType.KMH ? speed * 3.6f : speed * 2.237f;
    }
    //이동하면서 바퀴의 조향 기능도 있고 Rigidbody에 힘을 가해주는 기능으로 이동한다.
    public void Move(float _acceleration, float _steering, float _brake)
    {
        float nSteering = Mathf.Lerp(currentSteering, _steering, Time.deltaTime * steeringLerp); //현재, 원하는 스티어, 타임*러프 값
        currentSteering = nSteering;

        if(rb == null)
        {
            rb = GetComponent<Rigidbody>();
        }

        float angle = maxAngle * nSteering;
        float torque = maxTorque * _acceleration;
        float handbrake = _brake > 0f ? brakeTorque : 0f;

        foreach(var wheel in wheels)
        {
            //앞바퀴 조향
            if (wheel.transform.localPosition.z > 0)
            {
                wheel.steerAngle = angle;
            }
            // 뒷바퀴 조향
            if(wheel.transform.localPosition.z < 0)
            {
                wheel.brakeTorque = handbrake;
            }
            // 전륜이 아니면 뒷바퀴에 토크를 줘라
            if(wheel.transform.localPosition.z < 0 && driveType != DriveType.FrontWheelDrive)
            {
                wheel.motorTorque = torque;
            }
            // 후륜이 아니면 앞바퀴에 토크를 줘라
            if(wheel.transform.localPosition.z > 0 && driveType != DriveType.RearWheelDrive)
            {
                wheel.motorTorque = torque;
            }
            //휠 트랜스폼 정보를 위에 세팅한 값에 따라 변경함으로써 애니메이션 효과. 애니메이터 없이 코드로 돌리기
            if(animateWheels)
            {
                Quaternion rotation;
                Vector3 pos;
                wheel.GetWorldPose(out pos, out rotation);

                Transform shapeTransform = wheel.transform.GetChild(0); //첫번째 자식
                shapeTransform.position = pos;
                shapeTransform.rotation = rotation;
            }
        }

        if(rb != null)
        {
            //가속을 준다.
            float speedUnit = GetSpeedUnit(rb.velocity.magnitude);
            if(speedUnit > maxSpeed)
            {
                rb.velocity = GetSpeedMS(maxSpeed) * rb.velocity.normalized;
            }
            //downforce를 부여한다.
            rb.AddForce(-transform.up * downForce * rb.velocity.magnitude); //포스 모드를 설정 안하면 기본 설정으로 된다.
        }
    }
}

 

*Tooltip은 아래 사진과 같이 마우스를 변수명에 올렸을 때 설명이 나온다. 툴팁은 주석 역할도 할 수 있다.

 

 

VehicleControl 스크립트 생성

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

public class VehicleControl : MonoBehaviour
{
    private WheelDriveControl wheelDriveControl;
    private float initMaxSpeed = 0f;

    private void Start()
    {
        wheelDriveControl = GetComponent<WheelDriveControl>();
        initMaxSpeed = wheelDriveControl.maxSpeed;
    }

    private void Update()
    {
        float acceleration = 1f;
        float brake = 0f;
        float steering = 0f;
        wheelDriveControl.maxSpeed = initMaxSpeed;
        wheelDriveControl.Move(acceleration, steering, brake);
    }
}

 

비어있는 컴포넌트 추가하면 차량이 움직인다.

 

 


 

 

3인칭 카메라 설정

CameraControl 스크립트 생성

*Camera는 LateUpdate를 사용한다. 캐릭터가 다 움직이고 나서 업데이트 되어야 하기 때문.

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

public class CameraControl : MonoBehaviour
{
    private Transform myTransform = null;
    //타겟으로부터 떨어진 거리
    public float distance = 5f;
    //타겟으로부터의 높이
    public float height = 1.5f;
    //높이값 변경 속도
    public float heightDamping = 2.0f;
    //회전값 변경 속도
    public float rotationDamping = 3.0f;
    //타겟
    public Transform target = null;

    private void Start()
    {
        myTransform = GetComponent<Transform>();
        //타겟이 없다면 Player라는 태그를 가지고 있는 게임오브젝트가 타겟이다.
        if(target == null)
        {
            target = GameObject.FindWithTag("Player").transform;
        }
    }

    private void LateUpdate()
    {
        if(target == null)
        {
            return;
        }
        //카메라가 목표로 하고 있는 회전 Y축값과 높이값
        float wantedRotationAngle = target.eulerAngles.y;
        float wantedHeight = target.position.y + height;
        //현재 카메라가 바라보고 있는 회전 Y축값과 높이값
        float currentRotationAngle = myTransform.eulerAngles.y;
        float currentHeight = myTransform.position.y;
        //현재 카메라가 바라보고 있는 회전값과 높이값을 보간해서 새로운 값으로 계산
        currentRotationAngle = Mathf.LerpAngle(currentRotationAngle, wantedRotationAngle, rotationDamping * Time.deltaTime);
        currentHeight = Mathf.Lerp(currentHeight, wantedHeight, heightDamping * Time.deltaTime);
        //위에서 계산한 회전값으로 쿼터니언 회전값을 생성
        Quaternion currentRotation = Quaternion.Euler(0.0f, currentRotationAngle, 0.0f);
        //카메라가 타겟의 위치에서 회전하고자 하는 벡터만큼 뒤로 물러난다.
        myTransform.position = target.position;
        myTransform.position -= currentRotation * Vector3.forward * distance;
        //이동한 위치에서 원하는 높이값으로 올라간다.
        myTransform.position = new Vector3(myTransform.position.x, currentHeight, myTransform.position.z);
        //타겟을 항상 바라보도록 한다. forward -> target
        myTransform.LookAt(target);
    }
}

 

 


 

 

위의 코드에서 좌우 키로 자동차가 회전을 하게 만들 수 있다.

float steering = Input.GetAxisRaw("Horizontal");

 

 


 

 

TrafficHeadquarter 스크립트 생성

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

public class TrafficHeadquarter : MonoBehaviour
{
    //세그먼트와 세그먼트 사이의 검출 간격
    public float segDetectThresh = 0.1f;
    //웨이포인트의 크기
    public float waypointSize = 0.5f;
    //충돌 레이어들
    public string[] collisionLayers;
}

 

 

TrafficSegment 스크립트 생성

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

public class TrafficSegment : MonoBehaviour
{
    //다음에 이동할 segment들
    public List<TrafficSegment> nextSegments = new List<TrafficSegment>();
    //이 세그먼트의 ID 값
    public int ID = -1;
    //구간이 갖고있는 웨이포인트들 시작 -> 끝점 등 2~3개를 보통 가지고 있다.
    public List<TrafficWaypoint> waypoints = new List<TrafficWaypoint>();

    public bool IsOnSegment(Vector3 pos)
    {
        TrafficHeadquarter trafficHeadquarter = GetComponentInParent<TrafficHeadquarter>(); //부모것 가져오기
        
        for(int i = 0; i < waypoints.Count - 1; i++)
        {
            Vector3 pos1 = waypoints[i].transform.position;
            Vector3 pos2 = waypoints[i + 1].transform.position;
            //첫 번째 웨이포인트와 차량의 거리
            float d1 = Vector3.Distance(pos1, pos);
            //두 번째 웨이포인트와 차량의 거리
            float d2 = Vector3.Distance(pos2, pos);
            //첫 번째 웨이포인트와 두 번째 웨이포인트의 거리
            float d3 = Vector3.Distance(pos1, pos2);

            float diff = (d1 + d2) - d3;
            //값 사이에 있다면
            if(diff < trafficHeadquarter.segDetectThresh && diff > -trafficHeadquarter.segDetectThresh)
            {
                //자동차가 두 웨이포인트 사이에 가까이 있다.
                return true;
            }
        }
        //자동차가 두 웨이포인트 사이에서 멀리 있다. for문 끝나고 false
        return false;
    }
}

 

 

 

TrafficWaypoint 스크립트 생성

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

public class TrafficWaypoint : MonoBehaviour
{
    public TrafficSegment segment;

    //웨이포인트 중간에 있을 수 있는 콜라이더와 차량의 충돌 방지
    public void RemoveCollider()
    {
        if (GetComponent<SphereCollider>())
        {
            Debug.Log("Remove Collider");
            DestroyImmediate(gameObject.GetComponent<SphereCollider>());
        }
    }
    public void Refresh(int newID, TrafficSegment newSegment)
    {
        segment = newSegment;
        //WayPoint-1, WayPoint-10
        name = "WayPoint-" + newID.ToString();
        tag = "WayPoint";
        gameObject.layer = LayerMask.NameToLayer("Dafault");
        RemoveCollider();
    }
    public Vector3 GetVisualPos()
    {
        return transform.position + new Vector3(0.0f, 0.5f, 0.0f);
    }
}

 

 

TrafficHeadquarter 스크립트 수정

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

public class TrafficHeadquarter : MonoBehaviour
{
    //세그먼트와 세그먼트 사이의 검출 간격
    public float segDetectThresh = 0.1f;
    //웨이포인트의 크기
    public float waypointSize = 0.5f;
    //충돌 레이어들
    public string[] collisionLayers;

    public List<TrafficSegment> segments = new List<TrafficSegment>();
    public TrafficSegment curSegment;
    
    public List<TrafficWaypoint> GetAllWaypoints()
    {
        List<TrafficWaypoint> waypoints = new List<TrafficWaypoint>();
        foreach (var segment in segments)
        {
            waypoints.AddRange(segment.waypoints);
        }
        return waypoints;
    }

}

 

 

차 앞에 충돌 체크를 할 Raycast 추가할 예정. 차에 Anchor를 하나 붙인다.

 

 

VehicleControl 스크립트 수정

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using Unity.PlasticSCM.Editor.WebApi;

public class VehicleControl : MonoBehaviour
{
    private WheelDriveControl wheelDriveControl;
    private float initMaxSpeed = 0f;
    //자동차가 이동할 타겟 구조체
    public struct Target
    {
        public int segment;
        public int waypoint;
    }
    //자동차의 상태
    public enum Status
    {
        Go,
        Stop,
        SlowDown
    }
    [Header("교통 관제 시스템")]
    [Tooltip("현재 활성화 된 교통 시스템")]
    public TrafficHeadquarter trafficHeadquarter;
    [Tooltip("차량이 목표에 도달하는 시기를 확인한다. 다음 웨이포인트를 더 일찍 예상하는데 사용할 수 있습니다. (이 숫자가 높을 수록 더 빨리 예상됩니다.")]
    public float waypointThresh = 2.5f;

    [Header("차의 감지레이더")]
    [Tooltip("레이를 쏠 앵커")]
    public Transform raycastAnchor;
    [Tooltip("레이의 길이")]
    public float raycastLength = 3f;
    [Tooltip("레이 사이의 간격")]
    public float raycastSpacing = 3f;
    [Tooltip("생성될 레이의 수")]
    public int raycastNumber = 8;
    [Tooltip("감지된 차량이 이 거리 미만이면 차가 정지")]
    public float emergencyBrakeThresh = 1.5f;
    [Tooltip("이 거리보다 낮거나 거리보다 높을 경우 자동차의 속도가 느려짐")]
    public float slowDownThresh = 5f;

    public Status vehicleStatus = Status.Go;
    private int pastTargetSegment = -1;
    private Target currentTarget;
    private Target nextTarget;


    private void Start()
    {
        wheelDriveControl = GetComponent<WheelDriveControl>();
        initMaxSpeed = wheelDriveControl.maxSpeed;
        //없으면 찾아 넣어준다.
        if (raycastAnchor == null && transform.Find("Raycast Anchor") != null)
        {
            raycastAnchor = transform.Find("Raycast Anchor");
        }
    }

    private void Update()
    {
        //테스트 코드 주석처리
        //float acceleration = 1f;
        //float brake = 0f;
        //float steering = Input.GetAxisRaw("Horizontal");
        //wheelDriveControl.maxSpeed = initMaxSpeed;
        //wheelDriveControl.Move(acceleration, steering, brake);

        if(trafficHeadquarter == null)
        {
            return;
        }
    }
    int GetNextSegmentID()
    {
        //hq가 들고있는 구간 중에 현재 차량이 속해있는 세그먼트가 갖고있는 다음 구간들을 얻어온다.
        List<TrafficSegment> nextSegments = trafficHeadquarter.segments[currentTarget.segment].nextSegments;
        if(nextSegments.Count == 0 )
        {
            return 0;
        }

        int randomCount = UnityEngine.Random.Range(0, nextSegments.Count - 1);
        return nextSegments[randomCount].ID;
    }

    void SetWaypointVehicleIsOn()
    {
        foreach (var segment in trafficHeadquarter.segments)
        {
            //현재 차가 이 구간에 있는지 확인
            if (segment.IsOnSegment(transform.position))
            {
                currentTarget.segment = segment.ID;
                //구간 내에서 시작할 가장 가까운 웨이포인트 찾기
                float minDist = float.MaxValue;
                List<TrafficWaypoint> waypoints = trafficHeadquarter.segments[currentTarget.segment].waypoints;
                for(int j = 0; j < waypoints.Count; j++)
                {
                    float distance = Vector3.Distance(transform.position, waypoints[j].transform.position);

                    Vector3 lSpace = transform.InverseTransformPoint(waypoints[j].transform.position);
                    if(distance < minDist && lSpace.z > 0f)
                    {
                        minDist = distance;
                        currentTarget.waypoint = j;
                    }
                }

                break;
            }
        }
        //다음 target 찾기
        nextTarget.waypoint = currentTarget.waypoint + 1;
        nextTarget.segment = currentTarget.segment;
        //위에 지정한 다음 타겟의 waypoint가 범위를 벗어났다면 다시 처음 0번 째 waypoint. 다음 세그먼트 아이디를 구한다.
        if(nextTarget.waypoint >= trafficHeadquarter.segments[currentTarget.segment].waypoints.Count)
        {
            nextTarget.waypoint = 0;
            nextTarget.segment = GetNextSegmentID();
        }
    }
}
728x90
반응형