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

2024. 2. 22. 21:06SKKU DT

728x90
반응형

어제 만들었던 Editor 폴더 안에 스크립트 생성

TrafficHQEditor / TrafficHQEditorGizmo / TrafficHQInspectorEditor / TrafficIntersectionEditor / VehicleSettingEditor

 

 

TrafficHQEditorGizmo 스크립트 작성

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

public static class TrafficHQEditorGizmo
{
    //화살표를 그린다. 대각선 45도 방향 양쪽 2개
    private static void DrawArrow(Vector3 point, Vector3 forward, float size)
    {
        forward = forward.normalized * size;
        Vector3 left = Quaternion.Euler(0f, 45f, 0f) * forward;
        Vector3 right = Quaternion.Euler(0f, -45f, 0f) * forward;

        Gizmos.DrawLine(point, point + left);
        Gizmos.DrawLine(point, point + right);
    }
    //화살표 그림 타입에 따라 화살표 개수를 얻어온다.
    private static int GetArrowCount(Vector3 pointA, Vector3 pointB, TrafficHeadquarter headquarter)
    {
        switch(headquarter.arrowDrawType)
        {
            case TrafficHeadquarter.ArrowDraw.FixedCount:
                return headquarter.arrowCount;
                break;
            case TrafficHeadquarter.ArrowDraw.ByLength:
                int count = (int)(Vector3.Distance(pointA, pointB) / headquarter.arrowDistance);
                return Mathf.Max(1, count);
                break;
            case TrafficHeadquarter.ArrowDraw.Off:
                return 0;
            default:
                throw new ArgumentOutOfRangeException();
        }
    }
    //선택되었을 때나 선택되지 않았을 때도 활성화 상태라면 기즈모를 그립니다.
    [DrawGizmo(GizmoType.Selected | GizmoType.NonSelected | GizmoType.Active)] //속성이 [Flags]이기 때문에 or 연산이 가능
    private static void DrawGizmo(TrafficHeadquarter headquarter, GizmoType gizmoType)
    {
        //기즈모를 안그려야한다면 리턴
        if (headquarter.hideGizmos)
        {
            return;
        }
        foreach (TrafficSegment segment in headquarter.segments)
        {
            //세그먼트 이름 출력(ex. Segment-0), 빨간색
            GUIStyle style = new GUIStyle
            {
                normal =
                {
                    textColor = new Color(1, 0, 0)
                },
                fontSize = 15
            };
            Handles.Label(segment.transform.position, segment.name, style);
            //웨이포인트 그리기
            for (int j = 0; j < segment.waypoints.Count; j++)
            {
                //현재 웨이포인트 위치 찾고,
                Vector3 pos = segment.waypoints[j].GetVisualPos();
                //구 그리고, 방향을 표시하려면 색상을 변경
                Gizmos.color = new Color(0, 0, ((j + 1) / (float)segment.waypoints.Count), 1f);
                Gizmos.DrawSphere(pos, headquarter.waypointSize);
                //다음 웨이포인트 위치 찾고,
                Vector3 pNext = Vector3.zero;
                //다음 웨이포인트가 존재하면 가져오고
                if(j < segment.waypoints.Count - 1 && segment.waypoints[j + 1] != null)
                {
                    pNext = segment.waypoints[j + 1].GetVisualPos();
                }
                //다음 웨이포인트가 있다면
                if(pNext != Vector3.zero)
                {
                    //내가 작업중인 세그먼트라면 주황색
                    if(segment == headquarter.curSegment)
                    {
                        Gizmos.color = new Color(1f, 0.3f, 0.1f);
                    }
                    else
                    {
                        //그냥 웨이포인트면 빨간색
                        Gizmos.color = new Color(1f, 0f, 0f);
                    }
                    //선택한 구간(segment)은 녹색으로 그린다.
                    if(Selection.activeObject == segment.gameObject)
                    {
                        Gizmos.color = Color.green;
                    }
                    //두 웨이포인트의 연결선 그리기
                    Gizmos.DrawLine(pos, pNext);
                    //arrowDrawType을 기반으로 화살표 그릴 개수를 얻어와서
                    int arrowDrawCount = GetArrowCount(pos, pNext, headquarter);
                    //화살표를 그려주자
                    for(int i = 1; i < arrowDrawCount + 1; i++)
                    {
                        Vector3 point = Vector3.Lerp(pos, pNext, (float)i / (arrowDrawCount + 1));
                        DrawArrow(point, pos - pNext, headquarter.arrowSizeWaypoint);
                    }
                }
            }
            //세그먼트를 연결하는 선 그리기
            foreach(TrafficSegment nextSegment in segment.nextSegments)
            {
                if(nextSegment != null)
                {
                    Vector3 p1 = segment.waypoints.Last().GetVisualPos();
                    Vector3 p2 = nextSegment.waypoints.First().GetVisualPos();
                    //노란색 선으로 그려준다.
                    Gizmos.color = new Color(1f, 1f, 0f);
                    Gizmos.DrawLine(p1, p2);
                    if(headquarter.arrowDrawType != TrafficHeadquarter.ArrowDraw.Off)
                    {
                        DrawArrow((p1 + p2) / 2f, p1 - p2, headquarter.arrowSizeIntersection);
                    }
                }
            }
        }
    }
}

 

 


 

강사님의 VehicleControl 스크립트로 수정했다. (enum 변수 바꿔야 함)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Runtime.InteropServices;
using Unity.Collections;
using UnityEditor.Rendering;
using UnityEngine.Timeline;
using Random = UnityEngine.Random;

public class VehicleControl : MonoBehaviour
{
    private WheelDriveControl wheelDriveControl;
    private float initMaxSpeed = 0f;
    //자동차가 이동할 타겟 데이터 구조체.
    public struct Target
    {
        public int segment;
        public int waypoint;
        public override string ToString()
        {
            string ret = $"Target : {segment} / {waypoint}";
            return ret;
        }
    }
    //자동차의 상태.
    public enum Status
    {
        GO,
        STOP,
        SLOW_DOWN,
    }

    [Header("교통 관제 시스템.")] [Tooltip("현재 활성화 되 교통 시스템.")]
    public TrafficHeadquarter trafficHeadquarter;
    [Tooltip("차량이 목표에 도달한 시기를 확인합니다. 다음 웨이포인트를 더 일찍 예상하는데 사용할 수 있습니다.(이 숫자가 높을 수록" +
             "더 빨리 예상됩니다.")]
    public float waypointThresh = 2.5f;

    [Header("감지 레이더")] [Tooltip("레이를 쏠 앵커.")]
    public Transform raycastAnchor;
    [Tooltip("레이의 길이")]
    public float raycastLength = 3f;
    [Tooltip("레이 사이의 간격.")]
    public float raycasySpacing = 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;
    
    
    void Start()
    {
        wheelDriveControl = GetComponent<WheelDriveControl>();
        initMaxSpeed = wheelDriveControl.maxSpeed;

        if (raycastAnchor == null && transform.Find("Raycast Anchor") != null)
        {
            raycastAnchor = transform.Find("Raycast Anchor");
        }
        //시작하면 내가 어디에 있는지, 어디로 가야하는 지 한번 찾아봅니다.
        SetWaypointVehicleIsOn();
    }
    
    void Update()
    {
        //테스트 코드. 주석처리 합니다.
        //float accelation = 1f;
        //float brake = 0f;
        //float steering = Input.GetAxisRaw("Horizontal");// 0f;
        //wheelDriveControl.maxSpeed = initMaxSpeed;
        //wheelDriveControl.Move(accelation, steering, brake);

        if (trafficHeadquarter == null)
        {
            return;
        }
        //이동해야할 타겟 웨이포인트와 가까운지, 가깝다면 다음 웨이포인트 선정까지.
        WayPointChecker();
        //자율 주행.
        MoveVehicle();
    }

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

        int randomCount = 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번째 웨이포인트.다음 세그먼트 아이디를 구합니다.
        if (nextTarget.waypoint >= trafficHeadquarter.segments[currentTarget.segment].Waypoints.Count)
        {
            nextTarget.waypoint = 0;
            nextTarget.segment = GetNextSegmentID();
        }
    }
    //다음 이동할 웨이포인트를 체크하여 타겟을 선정.
    void WayPointChecker()
    {
        GameObject waypoint = trafficHeadquarter.segments[currentTarget.segment].
            Waypoints[currentTarget.waypoint].gameObject;
        //차량을 기준으로 한 다음 웨이포인트와의 위치를 찾기 위해 웨이포인트와의 거리 계산.
        Vector3 wpDist = transform.InverseTransformPoint(
            new Vector3(waypoint.transform.position.x,
                transform.position.y,
                waypoint.transform.position.z));
        //만약 현재 타겟으로 하고 있는 웨이포인트와 일정 거리 이하로 가깝다면.
        if (wpDist.magnitude < waypointThresh)
        {
            currentTarget.waypoint++;
            //현재 구간이 갖고 있는 웨이포인트를 다 돌았따면 다음 구간의 웨이포인트로 타겟을 변경.
            if (currentTarget.waypoint >= trafficHeadquarter.segments[currentTarget.segment].Waypoints.Count)
            {
                pastTargetSegment = currentTarget.segment;
                currentTarget.segment = nextTarget.segment;
                currentTarget.waypoint = 0;
            }
            //다음 타겟의 웨이포인트도 찾기.
            nextTarget.waypoint = currentTarget.waypoint + 1; //<--요거 고쳐주세요. 버그.
            if (nextTarget.waypoint >= trafficHeadquarter.segments[currentTarget.segment].Waypoints.Count)
            {
                nextTarget.waypoint = 0;
                nextTarget.segment = GetNextSegmentID();
            }
        }

    }
    //레이 캐스팅 함수. -> 충돌 레이어와 자동차 레이어만 레이 캐스팅합니다.
    void CastRay(Vector3 anchor, float angle, Vector3 dir, float length, out GameObject outObstacle,
        out float outHitDistance)
    {
        outObstacle = null;
        outHitDistance = -1;
        
        Debug.DrawRay(anchor, Quaternion.Euler(0f, angle, 0f)* dir * length,
            Color.red);
        //일단 자동차 레이어만.
        int layer = 1 << LayerMask.NameToLayer(TrafficHeadquarter.VehicleTagLayer);
        int finalMask = layer;
        
        //추가 충돌체의 레이어가 있으면 추가.
        foreach (var layerName in trafficHeadquarter.collisionLayers)
        {
            int id = 1 << LayerMask.NameToLayer(layerName);
            finalMask = finalMask | id;
        }

        RaycastHit hit;
        if (Physics.Raycast(anchor, Quaternion.Euler(0f, angle, 0f) * dir, 
            out hit, length, finalMask))
        {
            outObstacle = hit.collider.gameObject;
            outHitDistance = hit.distance;
            //Vector3 hitPosition = hit.point;
        }

    }
    //레이캐스팅을 해서 충돌체를 얻어오고 거리도 얻어오는 함수.
    GameObject GetDetectObstacles(out float hitDist)
    {
        GameObject obstacleObject = null;
        float minDist = 10000f;
        float initRay = (raycastNumber / 2f) * raycasySpacing;
        float hitDistance = -1f;

        for (float a = -initRay; a <= initRay; a += raycasySpacing)
        {
            CastRay(raycastAnchor.transform.position, a, transform.forward,
                raycastLength, out obstacleObject, out hitDistance);

            if (obstacleObject == null)
            {
                continue;
            }

            float dist = Vector3.Distance(transform.position, obstacleObject.transform.position);
            if (dist < minDist)
            {
                minDist = dist;
            }
        }

        hitDist = hitDistance;
        return obstacleObject;
    }
    //이 차량의 구간(segment)을 얻어오는 함수.
    public int GetSegmentVehicleIsIn()
    {
        int vehicleSegment = currentTarget.segment;
        bool isOnSegment = trafficHeadquarter.segments[vehicleSegment].IsOnSegment(transform.position);
        if (isOnSegment == false)
        {
            bool isOnPastSegment = trafficHeadquarter.segments[pastTargetSegment].IsOnSegment(transform.position);
            if (isOnPastSegment)
            {
                vehicleSegment = pastTargetSegment;
            }
        }

        return vehicleSegment;
    }
    //가급적 자율주행을 하고 싶습니다.
    void MoveVehicle()
    {
        //기본적으로 풀 엑셀, 노 브레이크, 노 핸들링.
        float acc = 1f;
        float brake = 0f;
        float steering = 0f;
        wheelDriveControl.maxSpeed = initMaxSpeed;
        if (currentTarget.segment >= trafficHeadquarter.segments.Count ||
            currentTarget.waypoint >= trafficHeadquarter.segments[currentTarget.segment].Waypoints.Count)
        {
            Debug.LogError(currentTarget.ToString());
        }
        Transform targetTransform = trafficHeadquarter.segments[currentTarget.segment].Waypoints[currentTarget.waypoint].transform;
        Transform nextTargetTransform = trafficHeadquarter.segments[nextTarget.segment].Waypoints[nextTarget.waypoint].transform;
        Vector3 nextVector3 = nextTargetTransform.position - targetTransform.position;
        //회전을 해야하는 지 계산.
        float nextSteering = Mathf.Clamp(transform.InverseTransformDirection(nextVector3.normalized).x,
            -1, 1);
        //만약 차가 서야 한다면.
        if (vehicleStatus == Status.STOP)
        {
            acc = 0f;
            brake = 1f;
            wheelDriveControl.maxSpeed = Mathf.Min(wheelDriveControl.maxSpeed / 2f, 5f);
        }
        else
        {
            // - ----- ----------- ---  -----
            //속도를 줄여야 하는 경우.
            if (vehicleStatus == Status.SLOW_DOWN)
            {
                acc = 0.3f;
                brake = 0f;
            }
            //회전을 해야 한다면 속도도 조절.
            if (nextSteering > 0.3f || nextSteering < -0.3f)
            {
                wheelDriveControl.maxSpeed = Mathf.Min(wheelDriveControl.maxSpeed,
                    wheelDriveControl.steeringSpeedMax);
            }
            //2. 레이캐스트로 감지되 장애물이 있는 지 확인.
            float hitDist;
            GameObject obstacle = GetDetectObstacles(out hitDist);
            // 무언가 충돌이 되었다면.
            if (obstacle != null)
            {
                WheelDriveControl obstacleVehicle = null;
                obstacleVehicle = obstacle.GetComponent<WheelDriveControl>();
                //자동차라면.
                if (obstacleVehicle != null)
                {
                    //앞차 인지 부터 확인.
                    float dotFront = Vector3.Dot(transform.forward,
                        obstacleVehicle.transform.forward);
                    //감지된 앞 차량의 속도가 내차의 속도보다 낮으면 속도를 줄인다.
                    if (dotFront > 0.8f && obstacleVehicle.maxSpeed < wheelDriveControl.maxSpeed)
                    {
                        //속도를 줄일때 아무리 작아도 0.1보다는 크게 조절.
                        float speed = Mathf.Max(
                            wheelDriveControl.GetSpeedMS(obstacleVehicle.maxSpeed) - 0.5f, 0.1f);
                        wheelDriveControl.maxSpeed = wheelDriveControl.GetSpeedUnit(speed);
                    }
                    //두 차량이 너무 가까우면서 같은 방향을 향하고 있으면 일단 멈춘다.
                    if (dotFront > 0.8f && hitDist < emergencyBrakeThresh)
                    {
                        acc = 0f;
                        brake = 1;
                        //아무리 속도를 줄여도 최소속도까지만 줄입니다.
                        wheelDriveControl.maxSpeed = Mathf.Max(wheelDriveControl.maxSpeed / 2f,
                            wheelDriveControl.minSpeed);
                    }
                    //두 차량이 너무 가까우면서 같은 방향을 향하고 있지 않은 경우 , 전면에서 달려오는 차와 부딪히게 생긴경우.
                    else if (dotFront <= 0.8f && hitDist < emergencyBrakeThresh)
                    {
                        acc = -0.3f;
                        brake = 0f;
                        wheelDriveControl.maxSpeed = Mathf.Max(wheelDriveControl.maxSpeed / 2f,
                            wheelDriveControl.minSpeed);
                        //가까이에 있는 차량이 오른쪽에 있을수 있고 왼쪽에 있을 수 있기 때문에 그에따라 회전도 하겠습니다.
                        float dotRight = Vector3.Dot(transform.forward, obstacleVehicle.transform.forward);
                        //오른쪽.
                        if (dotRight > 0.1f)
                        {
                            steering = -0.3f;
                        }
                        //왼쪽.
                        else if (dotRight < -0.1f)
                        {
                            steering = 0.3f;
                        }
                        //가운데.
                        else
                        {
                            steering = -0.7f;
                        }
                    }
                    //두 차량이 가까워지면 속도를 줄이자.
                    else if (hitDist < slowDownThresh)
                    {
                        acc = 0.5f;
                        brake = 0f;
                    }
                }
                //장애물....
                else
                {
                    //너무 가까우면 긴급 제동.
                    if (hitDist < emergencyBrakeThresh)
                    {
                        acc = 0f;
                        brake = 1f;
                        wheelDriveControl.maxSpeed = Mathf.Max(wheelDriveControl.maxSpeed / 2f,
                            wheelDriveControl.minSpeed);
                    }
                    //그렇지 않으면 일정 거리 이하로 가까워지면 천천히 이동.
                    else if (hitDist < slowDownThresh)
                    {
                        acc = 0.5f;
                        brake = 0f;
                    }
                }
            }
            
            //경로를 따르도록 방향을 조정해야하는 지 확인.
            if (acc > 0f)
            {
                Vector3 nextVector = trafficHeadquarter.segments[currentTarget.segment].Waypoints
                    [currentTarget.waypoint].transform.position - transform.position;
                steering = Mathf.Clamp(transform.InverseTransformDirection(nextVector.normalized).x,
                    -1, 1);
            }
        }
        //차량 이동.
        wheelDriveControl.Move(acc, steering, brake);
    }
}

 

강사님의 WheelDriveControl 스크립트로 수정했다.(enum 변수 바꿔야 함)

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

[RequireComponent(typeof(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 breakTorque = 100000f;
    [Tooltip("속도 단위.")]
    public SpeedUnitType unityType = 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;
                wheelshape.transform.localPosition = Vector3.zero;//이거.
            }
            else if (rightWheelShape != null && wheel.transform.localPosition.x > 0)
            {
                var wheelshape = Instantiate(rightWheelShape);
                wheelshape.transform.parent = wheel.transform;
                wheelshape.transform.localPosition = Vector3.zero;//이거.
            }
            wheel.ConfigureVehicleSubsteps(10,1,1);
        }
        
    }
    private void Awake()
    {
        Init();    
    }
    /*private void OnEnable()
    {
        Init();
    }*/
    //현재 속도 단위의 맞추어 현재 속도를 얻어옵니다.
    public float GetSpeedMS(float speed)
    {
        if (speed == 0f)
        {
            return 0f;
        }

        return unityType == SpeedUnitType.KMH ? speed / 3.6f : speed / 2.237f;
    }
    public float GetSpeedUnit(float speed)
    {
        return unityType == SpeedUnitType.KMH ? speed * 3.6f : speed * 2.237f;
    }
    //이동하면서 바퀴의 조향 기능도 있고 리지드바디에 힘을 가해주는 기능으로 이동한다.
    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 ? breakTorque : 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);
        }
    }
}

 

강사님 스크립트를 적용하니 잘 된다!

 

 


 

 

TrafficIntersectionEditor 스크립트 작성

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

public class TrafficIntersectionEditor : Editor
{
    private TrafficIntersection intersection;

    private void OnEnable()
    {
        intersection = target as TrafficIntersection;
    }

    public override void OnInspectorGUI()
    {
        intersection.IntersectionType = (IntersectionType)EditorGUILayout.EnumPopup("교차로 타입", intersection.IntersectionType); ;
        EditorGUI.BeginDisabledGroup(intersection.IntersectionType != IntersectionType.Stop);
        {
            InspectorHelper.Header("우선 멈춤 구간");
            InspectorHelper.PropertyField("우선 구간", "prioritySegments", serializedObject);
            serializedObject.ApplyModifiedProperties();
        }
        EditorGUI.EndDisabledGroup();

        EditorGUI.BeginDisabledGroup(intersection.intersectionType != IntersectionType.TRAFFIC_LIGHT);
        {
            InspectorHelper.Header("신호 교차로");
            InspectorHelper.FloatField("신호 시간 (초)", ref intersection.lightDuration);
            InspectorHelper.FloatField("주황불 시간 (초)", ref intersection.orangeLightDuration);
            InspectorHelper.PropertyField("첫 번째 빨간 불 그룹1", "lightGroup1", serializedObject);
            InspectorHelper.PropertyField("두 번째 빨간 불 그룹2", "lightGroup2", serializedObject);
            serializedObject.ApplyModifiedProperties();
        }
        EditorGUI.EndDisabledGroup();
    }
}

 

 

TrafficHQInspectorEditor 스크립트 작성

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

public class TrafficHQInspectorEditor
{
    public static void DrawInspector(TrafficHeadquarter trafficHeadquarter, SerializedObject serializedObject, out bool restructureSystem)
    {
        //기즈모 세팅
        InspectorHelper.Header("기즈모 설정");
        InspectorHelper.Toggle("기즈모를 숨길까요?", ref trafficHeadquarter.hideGizmos);
        //화살표 세팅
        InspectorHelper.DrawArrowTypeSelection(trafficHeadquarter);
        InspectorHelper.FloatField("웨이포인트 크기", ref trafficHeadquarter.Waypointsize);
        EditorGUILayout.Space();
        //시스템 설정
        InspectorHelper.Header("시스템 설정");
        InspectorHelper.FloatField("구간 감지 최소 거리", ref trafficHeadquarter.segDetectThresh);
        InspectorHelper.PropertyField("충돌 레이어들, ", "collisionLayers", serializedObject);
        EditorGUILayout.Space();
        //도움말
        InspectorHelper.HelpBox("Ctrl + 마우스 왼쪽 : 세그먼트 생성 \n " + "Shift + 마우스 왼쪽 : 웨이포인트 생성 \n" + "Alt + 마우스 왼쪽 : 교차로 생성");
        InspectorHelper.HelpBox("차량은 추가한대로 웨이포인트를 따라서 이동하게 됩니다.");

        EditorGUILayout.Space();
        restructureSystem = InspectorHelper.Button("교통 시뮬레이션 시스템 재구성");
    }
}

 

 

VehicleSettingEditor 스크립트 작성

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using Unity.Mathematics;

public class VehicleSettingEditor : Editor
{
    //휠콜라이더 세부 설정을 툴에서 관리
    private static void SetupWheelCollider(WheelCollider collider)
    {
        collider.mass = 20f;
        collider.radius = 0.175f;
        collider.wheelDampingRate = 0.25f;
        collider.suspensionDistance = 0.05f;
        collider.forceAppPointDistance = 0f;

        JointSpring jointSpring = new JointSpring();
        jointSpring.spring = 70000f;
        jointSpring.damper = 3500f;
        jointSpring.targetPosition = 1f;
        collider.suspensionSpring = jointSpring;

        WheelFrictionCurve frictionCurve = new WheelFrictionCurve();
        frictionCurve.extremumSlip = 1f;
        frictionCurve.extremumValue = 1f;
        frictionCurve.asymptoteSlip = 1f;
        frictionCurve.asymptoteValue = 1f;
        frictionCurve.stiffness = 1f;
        collider.forwardFriction = frictionCurve;
        collider.sidewaysFriction = frictionCurve;
    }
    [MenuItem("Component/TrafficTool/Setup Vehicle")]
    private static void SetupVehicle()
    {
        EditorHelper.SetUndoGroup("Setup Vehicle");
        //현재 차를 선택했다면(아직 세팅되지 않은)
        GameObject selected = Selection.activeGameObject;

        //프리팹 언팩. 오리지널 프리팹을 끊고 내부 수정 가능.(자식 오브젝트 추가/삭제)
        PrefabUtility.UnpackPrefabInstance(selected, PrefabUnpackMode.Completely, InteractionMode.AutomatedAction);

        //레이캐스트 앵커 만들기
        GameObject anchor = EditorHelper.CreateGameObject("Raycast Anchor", selected.transform);
        anchor.transform.localPosition = Vector3.zero;
        anchor.transform.localRotation = Quaternion.identity;
        //스크립트들 설정
        VehicleControl vehicleControl = EditorHelper.AddComponent<VehicleControl>(selected);
        vehicleControl.raycastAnchor = anchor.transform;
        //바퀴 메쉬 찾고
        Transform tireBackLeft = selected.transform.Find("Tire BackLeft");
        Transform tireBackRight = selected.transform.Find("Tire BackRight");
        Transform tireFrontLeft = selected.transform.Find("Tire FrontLeft");
        Transform tireFrontRight = selected.transform.Find("Tire FrontRight");
        //휠 콜라이더 세팅하고 바퀴를 휠 콜라이더의 차일드로 붙입니다.
        GameObject backLeftWheel = EditorHelper.CreateGameObject("TireBackLeft Wheel", selected.transform);
        backLeftWheel.transform.position = tireBackLeft.position;
        GameObject backRightWheel = EditorHelper.CreateGameObject("TireBackRight Wheel", selected.transform);
        backRightWheel.transform.position = tireBackRight.position;
        GameObject frontLeftWheel = EditorHelper.CreateGameObject("TireFrontLeft Wheel", selected.transform);
        frontLeftWheel.transform.position = tireFrontLeft.position;
        GameObject frontRightWheel = EditorHelper.CreateGameObject("TireFrontRight Wheel", selected.transform);
        frontRightWheel.transform.position = tireFrontRight.position;

        WheelCollider wheelCollider1 = EditorHelper.AddComponent<WheelCollider>(backLeftWheel);
        WheelCollider wheelCollider2 = EditorHelper.AddComponent<WheelCollider>(backRightWheel);
        WheelCollider wheelCollider3 = EditorHelper.AddComponent<WheelCollider>(frontLeftWheel);
        WheelCollider wheelCollider4 = EditorHelper.AddComponent<WheelCollider>(frontRightWheel);
        SetupWheelCollider(wheelCollider1);
        SetupWheelCollider(wheelCollider2);
        SetupWheelCollider(wheelCollider3);
        SetupWheelCollider(wheelCollider4);

        tireBackLeft.parent = backLeftWheel.transform;
        tireBackLeft.localPosition = Vector3.zero;
        tireBackRight.parent = backRightWheel.transform;
        tireBackRight.localPosition = Vector3.zero;
        tireFrontLeft.parent = frontLeftWheel.transform;
        tireFrontLeft.localPosition = Vector3.zero;
        tireFrontRight.parent = frontRightWheel.transform;
        tireFrontRight.localPosition = Vector3.zero;

        //WheelDrivecontrol 스크립트 붙이기
        WheelDriveControl wheelDriveControl = EditorHelper.AddComponent<WheelDriveControl>(selected);
        wheelDriveControl.Init();

        //Rigidbody 세팅
        Rigidbody rb = selected.GetComponent<Rigidbody>();
        rb.mass = 900f;
        rb.drag = 0.1f;
        rb.angularDrag = 3f;
        rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;

        //HeadQuarter 연결
        TrafficHeadquarter headquarter = FindObjectOfType<TrafficHeadquarter>();
        if(headquarter != null)
        {
            vehicleControl.trafficHeadquarter = headquarter;
        }

        //바디 콜라이더 붙이기
        BoxCollider boxCollider = EditorHelper.AddComponent<BoxCollider>(selected);
        boxCollider.isTrigger = true;

        GameObject colliders = EditorHelper.CreateGameObject("Colliders", selected.transform);
        colliders.transform.localPosition = Vector3.zero;
        colliders.transform.localRotation = Quaternion.identity;
        colliders.transform.localScale = Vector3.one;
        GameObject body = EditorHelper.CreateGameObject("Body", colliders.transform);
        body.transform.localPosition = Vector3.zero;
        body.transform.localRotation = Quaternion.identity;
        body.transform.localScale = Vector3.one;
        BoxCollider bodyCollider = EditorHelper.AddComponent<BoxCollider>(body);
        bodyCollider.center = new Vector3(0f, 0.4f, 0f);
        bodyCollider.size = new Vector3(0.95f, 0.54f, 2.0f);

        // 레이어까지 자동 주행 레이어 AutonomousVehicle set
        //만약 레이어가 없다면 엔진에 추가
        EditorHelper.CreateLayer(TrafficHeadquarter.VehicleTagLayer);
        selected.tag = TrafficHeadquarter.VehicleTagLayer;
        EditorHelper.SetLayer(selected, LayerMask.NameToLayer(TrafficHeadquarter.VehicleTagLayer), true);
        //위에 세팅한 값을 한꺼번에 undo하도록
        Undo.CollapseUndoOperations(Undo.GetCurrentGroup());
    }
}

 

 

[Component] 탭에 [TrafficTool] 메뉴가 생겼고, 새 차에 Setup Vehicle 하면 한 번에 세팅이 된다.

 

실행하면 새로 넣은 차도 바로 이동하는 것을 볼 수 있다.

 

 

TrafficHQEditor 스크립트 작성

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Unity.VisualScripting;
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(TrafficHeadquarter))]
public class TrafficHQEditor : Editor
{
    private TrafficHeadquarter headquarter;
    //웨이포인트 설치할때 필요한 임시 저장소들.
    private Vector3 startPosition;
    private Vector3 lastPoint;
    private TrafficWaypoint lastWaypoint;
    //traffic 시뮬레이터의 기반이되는 스크립트들 생성.
    [MenuItem("Component/TrafficTool/Create Traffic System")]
    private static void CreateTrafficSystem()
    {
        EditorHelper.SetUndoGroup("Create Traffic System");

        GameObject headquarterObject = EditorHelper.CreateGameObject("Traffic Headquarter");
        EditorHelper.AddComponent<TrafficHeadquarter>(headquarterObject);

        GameObject segmentsObject = EditorHelper.CreateGameObject("Segments",
            headquarterObject.transform);
        GameObject intersectionsObject = EditorHelper.CreateGameObject("Intersections",
            headquarterObject.transform);

        Undo.CollapseUndoOperations(Undo.GetCurrentGroup());
    }
    private void OnEnable()
    {
        headquarter = target as TrafficHeadquarter;
    }

    //웨이포인트 추가.
    private void AddWaypoint(Vector3 position)
    {
        //웨이포인트 게임오브젝트를 새로 생성.
        GameObject go = EditorHelper.CreateGameObject(
            "Waypoint-" + headquarter.curSegment.Waypoints.Count,
            headquarter.curSegment.transform);
        //위치는 내가 클릭한 곳으로 합니다.
        go.transform.position = position;
        TrafficWaypoint waypoint = EditorHelper.AddComponent<TrafficWaypoint>(go);
        waypoint.Refresh(headquarter.curSegment.Waypoints.Count,
            headquarter.curSegment);
        Undo.RecordObject(headquarter.curSegment, "");
        //HQ에 생선한 웨이포인트를 현재 작업중인 세그먼트에 추가합니다.
        headquarter.curSegment.Waypoints.Add(waypoint);
    }

    //세그먼트 추가.
    private void AddSegment(Vector3 position)
    {
        int segID = headquarter.segments.Count;
        //Segments라고 만든 빈 게임오브젝트의 차일드로 세그먼트 게임오브젝트를 생성합니다.
        GameObject segGameObject = EditorHelper.CreateGameObject(
            "Segment-" + segID, headquarter.transform.GetChild(0).transform);
        //내가 지금 클릭한 위치에 세그먼트를 이동시킵니다.
        segGameObject.transform.position = position;
        //HQ에 현재 작업중인 세그먼트에 새로 만든 세그먼트 스크립트를 연결해줍니다.
        //이후에 추가되는 웨이포인트는 현재 작업중인 세그먼트에 추가되게 됩니다.
        headquarter.curSegment = EditorHelper.AddComponent<TrafficSegment>(segGameObject);
        headquarter.curSegment.ID = segID;
        headquarter.curSegment.Waypoints = new List<TrafficWaypoint>();
        headquarter.curSegment.nextSegments = new List<TrafficSegment>();

        Undo.RecordObject(headquarter, "");
        headquarter.segments.Add(headquarter.curSegment);
    }

    //인터섹션 추가.
    private void AddIntersection(Vector3 position)
    {
        int intID = headquarter.intersections.Count;
        //새로운 교차로구간을 만들어서 Intersections 게임오브젝트 차일드로 붙여줍니다.
        GameObject intersection = EditorHelper.CreateGameObject(
            "Intersection-" + intID, headquarter.transform.GetChild(1).transform);
        intersection.transform.position = position;

        BoxCollider boxCollider = EditorHelper.AddComponent<BoxCollider>(intersection);
        boxCollider.isTrigger = true;
        TrafficIntersection trafficIntersection = EditorHelper.AddComponent<TrafficIntersection>(
            intersection);
        trafficIntersection.ID = intID;

        Undo.RecordObject(headquarter, "");
        headquarter.intersections.Add(trafficIntersection);
    }
    //씬에서 직접 설치해보도록 하겠습니다.
    //Shift 
    //Control
    //Alt
    private void OnSceneGUI()
    {
        //마우스 클릭 조작이 있었는지 얻어옵니다.
        Event @event = Event.current;
        if (@event == null)
        {
            return;
        }
        //마우스포지션 위치로 레이를 만들어줍니다.
        Ray ray = HandleUtility.GUIPointToWorldRay(@event.mousePosition);
        RaycastHit hit;
        //마우스 위치로 충돌체 검출이 되었고, 마우스 왼쪽 클릭으로 인해 발생하였다.
        //0 왼쪽 클릭 1 오른쪽 클릭 2는 휠버튼
        if (Physics.Raycast(ray, out hit) &&
            @event.type == EventType.MouseDown &&
            @event.button == 0)
        {
            //마우스 왼쪽 클릭 + Shift -> 웨이포인트 추가.
            if (@event.shift)
            {
                //구간없는 웨이포인트는 존재할 수 없습니다.
                if (headquarter.curSegment == null)
                {
                    Debug.LogWarning("세그먼트 먼저 만들어주세요.");
                    return;
                }
                EditorHelper.BeginUndoGroup("Add WayPoint", headquarter);
                AddWaypoint(hit.point);
                Undo.CollapseUndoOperations(Undo.GetCurrentGroup());
            }
            //마우스 왼쪽 클릭 + Control -> 세그먼트 추가.
            //첫번째 웨이포인트도 같이 추가됩니다.
            else if (@event.control)
            {
                EditorHelper.BeginUndoGroup("Add Segment", headquarter);
                AddSegment(hit.point);
                AddWaypoint(hit.point);
                Undo.CollapseUndoOperations(Undo.GetCurrentGroup());
            }
            //마우스 왼쪽 클릭 + Alt -> 인터섹션 추가.
            else if (@event.alt)
            {
                EditorHelper.BeginUndoGroup("Add Intersection", headquarter);
                AddIntersection(hit.point);
                Undo.CollapseUndoOperations(Undo.GetCurrentGroup());
            }
        }
        //웨이포인트 시스템을 히어라키뷰에서 선택한 게임 객체로 설정.
        Selection.activeGameObject = headquarter.gameObject;
        //선택한 웨이포인트를 처리합니다.
        if (lastWaypoint != null)
        {
            //레이가 충돌할 수 있도록 Plane을 사용합니다.
            Plane plane = new Plane(Vector3.up, lastWaypoint.GetVisualPos());
            plane.Raycast(ray, out float dst);
            Vector3 hitPoint = ray.GetPoint(dst);
            //마우스 버튼을 처음 눌렀을때, LastPoint 재설정.
            if (@event.type == EventType.MouseDown && @event.button == 0)
            {
                lastPoint = hitPoint;
                startPosition = lastWaypoint.transform.position;
            }
            //선택한 웨이포인트를 이동.
            if (@event.type == EventType.MouseDrag && @event.button == 0)
            {
                Vector3 realPos = new Vector3(
                    hitPoint.x - lastPoint.x, 0, hitPoint.z - lastPoint.z);
                lastWaypoint.transform.position += realPos;
                lastPoint = hitPoint;
            }
            //선택한 웨이포인트를 해제.
            if (@event.type == EventType.MouseUp && @event.button == 0)
            {
                Vector3 curPos = lastWaypoint.transform.position;
                lastWaypoint.transform.position = startPosition;
                Undo.RegisterFullObjectHierarchyUndo(lastWaypoint, "Move WayPoint");
                lastWaypoint.transform.position = curPos;
            }
            //구 하나 그리겠습니다.
            Handles.SphereHandleCap(0, lastWaypoint.GetVisualPos(), Quaternion.identity,
                headquarter.waypointSize * 2f, EventType.Repaint);
            HandleUtility.AddDefaultControl(GUIUtility.GetControlID(FocusType.Passive));
            SceneView.RepaintAll();
        }
        //모든 웨이포인트로부터 판별식을 통해 충돌되는 웨이포인트를 lastWaypoint로 세팅.
        if (lastWaypoint == null)
        {
            //모든 웨이포인트한테 지금 내가 마우스 위치에서 생성한 레이가 충돌되는지 확인.
            lastWaypoint = headquarter.GetAllWaypoints()
                .FirstOrDefault(
                    i => EditorHelper.SphereHit(i.GetVisualPos(),
                        headquarter.waypointSize, ray));
        }
        //HQ의 현재 수정중인 세그먼트를 현재 선택한 세그먼트로 대체합니다.
        if (lastWaypoint != null && @event.type == EventType.MouseDown)
        {
            headquarter.curSegment = lastWaypoint.segment;
        }
        //현재 웨이포인트를 재설정.마우스를 이동하면 선택이 풀리도록.
        else if (lastWaypoint != null && @event.type == EventType.MouseMove)
        {
            lastWaypoint = null;
        }

    }
}

 

 

위의 코드를 성공적으로 작성했다면,

씬에서, [Component] - [TrafficTool] - [Create Traffic System] 메뉴를 통해 Traffic Headquarter 시스템을 만들 수 있다.

 

 

그리고 [Shift + 좌클릭]을 통해서 세그먼트 만들고 [Ctrl + 좌클릭]으로 웨이포인트 만들고 [Alt + 좌클릭]으로 교차로를 만들 수 있다.

 

 

이후 자동차 모델을 끌어놓고, [Component] - [Traffic Tool] - [Setup Vehicle]을 눌러 차량을 세팅하면 웨이포인트를 따라 차가 움직이는 것을 볼 수 있다.

728x90
반응형