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

2024. 2. 21. 19:02SKKU DT

728x90
반응형

TrafficHeadquarter

자율 주행차가 구간에서 구간을 이동할 때 다음 구간을 검색하기 위한 최소 거리 조절.

웨이포인트 크기 조정 가능

충돌체 레이어 관리

모든 구간을 갖고 있음

모든 교차로 갖고 있음

 

TrafficSegment

다음에 연결된 구간들

웨이포인트 갖고 있음

특정 위치를 중심으로 세그먼트 안에 있는지 검사

 

TrafficWaypoint

부모 Segment

스스로 충돌체 제거.(웨이포인트 충돌되면 안된다. 위치만 표시)

리프레쉬 기능

 

VehicleControl

WheelDriveControl 갖고 있음

Headquarter 를 알고 있음

웨이포인트 검색 최소 거리를 갖고 있음

충돌 검출을 위한 Raycast를 갖고 있음.(부채꼴 모양으로 ray를 쏴서 충돌체 검출)

어플리케이션이 실행되면 이 자동차가 현재 어느 구간인지, 어느 웨이포인트로 가야하는지 스스로 검색해서 찾아내는 기능

웨이포인트에서 다음 웨이포인트를 랜덤하게(직전 또는 좌회전) 선택하는 기능

주행할 때 이동 타겟 웨이포인트와의 거리를 실시간으로 체크해서 최소거리보다 짧으면 다음 웨이포인트 찾아보기

현재 어느 구간을 달리고 있는 지 얻어오는 기능

자율 주행

차량의 신호 인지 기능 -현재 구간에 신호가 무엇인지 아는 기능

 

TrafficIntersection

교차로 타입 -우선 정지, 신호등, 긴급 발생, 속도를 낮추는 타입

우선 멈춤 구간들

신호등 구간들

빨간불 그룹 2개를 갖고있음

교차로에 큰 트리거 Box를 놓고 자동차 트리거 콜라이더가 들어오면 이벤트 발생. -교차로에 진입한 차량을 Queue에 저장할 수 있음

교차로 구간에 있는 차량을 저장할 수 있다.

HQ를 알고 있다.

특정 구간이 빨간불인지 특정하기

교차로에 진입한 차를 이동시키는 기능

신호등 신호 변경 기능

차량이 이미 교차로에 있는 차인지?

이 구간이 우선 정지 차로인지?

트리거 제어하는 기

 


 

 

WheelDriveControl 코드 수정

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); //초기 세팅 (시작할 때 얼마나 큰 힘이 필요한가, 얼마나 줄어들 것인가, )
}
wheelShape.transform.localPosition = Vector3.zero;

두 줄을 추가하였다. 바퀴가 붙지 않는 현상을 해결하기 위해서.

 

 

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 const string VehicleTagLayer = "AutonomousVehicle";

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

}
public const string VehicleTagLayer = "AutonomousVehicle";

한 줄 추가

 

 


 

 

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");
        }
        //시작하면 내가 어디에 있는지 세그먼트 확인, 어디로 가야하는지 찾아본다.
        SetWaypointVehicleIsOn();
    }

    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;
        }
        //이동해야할 타겟 웨이포인트와 가까운지, 가깝다면 다음 웨이포인트 선정까지.
        WayPointChecker();
        //자유주행
        MoveVehicle();
    }
    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();
        }
    }
    //다음 이동할 웨이포인트를 체크하여 타겟을 선정.
    void WayPointChecker()
    {
        GameObject waypoint = trafficHeadquarter.segments[currentTarget.segment].waypoints[currentTarget.waypoint].gameObject;
        //차량을 기준으로 한 다음 웨이포인트와의 위치를 찾기 위해 웨이포인트와의 거리 계산.
        Vector3 wpDist = transform.InverseTransformDirection(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 outHitDistace)
    {
        outObstacle = null;
        outHitDistace = -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;
            outHitDistace = hit.distance;
            //Vector3 hitPosition = hit.point;
        }
    }
    //레이캐스팅을 해서 충돌체를 얻어오고 거리도 얻어오는 함수
    GameObject GetDetectObstacles(out float hitDist)
    {
        GameObject obstacleObject = null;
        float minDist = 10000f;
        float initRay = (raycastNumber / 2f) * raycastSpacing;
        float hitDistance = -1f;

        for(float a = -initRay; a <= initRay; a+= raycastSpacing)
        {
            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;

        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;
        //x포지션이 좌우로 핸들링, 회전을 해야하는 지 계산
        float nextSteering = Mathf.Clamp(transform.InverseTransformDirection(nextVector3.normalized).x, -1, 1);

        //만약 차가 서야 한다면
        if(vehicleStatus == Status.Stop)
        {
            acc = 0f;
            brake = 1f;
            wheelDriveControl.maxSpeed = Math.Min(wheelDriveControl.maxSpeed / 2f, 5f);
        }
        else
        {
            //속도를 줄여야 하는 경우
            if(vehicleStatus == Status.SlowDown)
            {
                acc = 0.3f; //결국 멈추게 된다.
                brake = 0f;

            }
            //회전을 해야 한다면 속도도 조절
            if(nextSteering > 0.3f || nextSteering < -0.3f)
            {
                wheelDriveControl.maxSpeed = Mathf.Min(wheelDriveControl.maxSpeed, wheelDriveControl.steeringSpeedMax);
            }
            //레이캐스트로 감지된 장애물이 있는지 확인
            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 = 1f;
                        //아무리 속도를 줄여도 최소속도까지만 줄여서 차가 전복되는 현상을 방지
                        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);
    }
}

 

 


 

 

**Behaviour Tree -여러 조건의 행동을 통제할 수 있다.

https://github.com/Qriva/MonoBehaviourTree

 

GitHub - Qriva/MonoBehaviourTree: Simple event driven Behaviour tree for Unity projects

Simple event driven Behaviour tree for Unity projects - Qriva/MonoBehaviourTree

github.com

 

 


 

 

TrafficHeadquarter 빈 오브젝트 만들고 스크립트 넣기

 

빈 오브젝트로 Waypoint 만들고 스크립트 넣기

 

Segment에도 스크립트 넣기

 

Segment 스크립트의 ID 수정, 빈 컴포넌트 자리에 끌어다 놓기

 

Waypoint 스크립트에도 끌어다놓기

 

Waypoint를 배치하기

 

차량의 VehicleControl 스크립트에 TrafficHeadquarter 끌어다 놓기

 

TrafficHeadquarter 스크립트에도 빈 컴포넌트 끌어다놓기

 

일단 자동차는 잘 움직인다.

 

여러대를 배치하면 돌아다니는 것을 볼 수 있다. (사고도 난다)

 

나중에 Segment를 만들고 끌어다 놓았던 것들을 툴을 만들어서 사용하기 편리하게 바꿔줄 것이다.

 

 


 

 

TrafficIntersection 스크립트 생성

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

public enum IntersectionType
{
    None = 0,
    Stop, //우선 멈춤 구간
    TrafficLight, //신호등
    TrafficSlow, //서행 구간
    Emergency //긴급 상황
}

public class TrafficIntersection : MonoBehaviour
{
    public IntersectionType intersectionType = IntersectionType.None;
    public int ID = -1;
    //우선 멈춤 구간들
    public List<TrafficSegment> prioritySegments = new List<TrafficSegment>();
    //신호등 구간에 필요한 속성들
    public float lightDuration = 8f;
    private float lastChangeLightTime = 0f;
    private Coroutine lightRoutine;
    public float lightRepeatRate = 8f;
    public float orangeLightDuration = 2f;
    //빨간 불 구간
    public List<TrafficSegment> lightGroup1 = new List<TrafficSegment>();
    public List<TrafficSegment> lightGroup2 = new List<TrafficSegment>();
    //교차로 영역에 있는 자동차들을 가지고 있음
    private List<GameObject> vehicleQueue = new List<GameObject>();
    private List<GameObject> vehiclesInIntersection = new List<GameObject>();
    private TrafficHeadquarter trafficHeadquarter;
    //현재 빨간불 그룹
    public int currentRedLightGroup = 1;

    //빨간불 구간입니까?
    bool IsRedLightSegment(int vehicleSegment)
    {
        if(currentRedLightGroup == 1)
        {
            foreach(var segment in lightGroup1 )
            {
                if (segment.ID == vehicleSegment)
                {
                    return true;
                }
            }
        }
        else if(currentRedLightGroup == 2)
        {
            foreach(var segment in lightGroup2)
            {
                if(segment.ID == vehicleSegment)
                {
                    return true;
                }
            }
        }
        return false;
    }
    //교차로에 진입한 자동차들을 이동시킴
    void MoveVehicleQueue()
    {
        //큐에 있는 모든 자동차 이동, 빨간불 신호 구간이 아닌 자동차들을 이동시킨다.
        List<GameObject> newVehicleQueue = new List<GameObject>(vehicleQueue);
        foreach(var vehicle in vehicleQueue)
        {
            VehicleControl vehicleControl = vehicle.GetComponent<VehicleControl>();
            int vehicleSegment = vehicleControl.GetSegmentVehicleIsIn();
            //빨간불 신호를 받지 않은 차량이라면,
            if(IsRedLightSegment(vehicleSegment) == false)
            {
                vehicleControl.vehicleStatus = VehicleControl.Status.Go;
                newVehicleQueue.Remove(vehicle);
            }
        }
        
        vehicleQueue = newVehicleQueue;
    }
    //신호 변경해주고, 차량 이동까지.
    void SwitchLights()
    {
        if(currentRedLightGroup == 1)
        {
            currentRedLightGroup = 2;
        }
        else if(currentRedLightGroup == 2)
        {
            currentRedLightGroup = 1;
        }
        else
        {
            currentRedLightGroup = 1;
        }
        //다른 차량을 움직이게 하기 전에 신호 전환 후 몇 초동안 기다리게 하기(주황불)
        Invoke("MoveVehicleQueue", orangeLightDuration);
    }

    private void Start()
    {
        vehicleQueue = new List<GameObject>();
        vehiclesInIntersection = new List<GameObject>();
        lastChangeLightTime = Time.time;
    }
    //코루틴으로 신호 변경 호출. 일정 간격(lightRepeatRate, lightDuration)
    private IEnumerator OnTrafficLight()
    {
        SwitchLights();
        yield return new WaitForSeconds(lightRepeatRate);
    }

    private void Update()
    {
        switch(intersectionType)
        {
            //신호등 교차로라면 일정 시간 별로 신호 교체를 한다.
            case IntersectionType.TrafficLight:
                if(Time.time > lastChangeLightTime + lightDuration)
                {
                    lastChangeLightTime = Time.time;
                    lightRoutine = StartCoroutine("OnTrafficLight");
                }
                break;
            //긴급 상황이라면 신호 교체 멈추고 0번 그룹으로 세팅한다.
            case IntersectionType.Emergency:
                if(lightRoutine != null)
                {
                    StopCoroutine(lightRoutine);
                    currentRedLightGroup = 0;
                }
                break;
            case IntersectionType.Stop:
                break;
            default:
                break;
        }
    }

    bool IsAlreadyInIntersection(GameObject target)
    {
        foreach(var vehicle in vehiclesInIntersection)
        {
            if(vehicle.GetInstanceID() == target.GetInstanceID())//같다는 얘기
            {
                return true;
            }
        }
        foreach(var vehicle in vehicleQueue)
        {
            if(vehicle.GetInstanceID() == target.GetInstanceID())
            {
                return true;
            }
        }
        return false;
    }
    //우선 구간이 있는지
    bool isPrioritySegment(int vehicleSegment)
    {
        foreach(var segment in prioritySegments)
        {
            if(vehicleSegment == segment.ID)
            {
                return true;
            }
        }
        return false;
    }
    //우선 멈춤 구간 트리거
    void TriggerStop(GameObject vehicle)
    {
        VehicleControl vehicleControl = vehicle.GetComponent<VehicleControl>();
        //웨이포인트 임계값에 따라 자동차는 대상 구간 또는 바로 직전 구간에 있을 수 있다. (세그먼트가 겹치지 않는 구간)
        int vehicleSegment = vehicleControl.GetSegmentVehicleIsIn();

        if (isPrioritySegment(vehicleSegment) == false)
        {
            //교차로에 차가 한 대라도 있다면 큐에 넣고 대기
            if(vehicleQueue.Count > 0 || vehiclesInIntersection.Count > 0)
            {
                vehicleControl.vehicleStatus = VehicleControl.Status.Stop;
                vehicleQueue.Add(vehicle);
            }
            //교차로에 차가 없다면
            else
            {
                vehiclesInIntersection.Add(vehicle);
                vehicleControl.vehicleStatus = VehicleControl.Status.SlowDown;
            }
        }
        else
        {
            vehicleControl.vehicleStatus = VehicleControl.Status.SlowDown;
            vehiclesInIntersection.Add(vehicle);
        }
    }
    //우선 멈춤에서 빠져나가기
    void ExitStop(GameObject vehicle)
    {
        vehicle.GetComponent<VehicleControl>().vehicleStatus = VehicleControl.Status.Go;
        vehiclesInIntersection.Remove(vehicle);
        vehicleQueue.Remove(vehicle);

        if(vehicleQueue.Count > 0 && vehiclesInIntersection.Count == 0)
        {
            vehicleQueue[0].GetComponent<VehicleControl>().vehicleStatus = VehicleControl.Status.Go;
        }
    }
    //신호 교차로 트리거. 차량을 멈추거나 이동시키거나
    void TriggerLight(GameObject vehicle)
    {
        VehicleControl vehicleControl = vehicle.GetComponent<VehicleControl>();
        int vehicleSegment = vehicleControl.GetSegmentVehicleIsIn();

        if(IsRedLightSegment(vehicleSegment))
        {
            vehicleControl.vehicleStatus = VehicleControl.Status.Stop;
            vehicleQueue.Add(vehicle);
        }
        else
        {
            vehicleControl.vehicleStatus = VehicleControl.Status.Go;
        }
    }
    //신호등 교차로 구간을 빠져나갔다면 그대로 이동
    void ExitLight(GameObject vehicle)
    {
        vehicle.GetComponent<VehicleControl>().vehicleStatus = VehicleControl.Status.Go;
    }
    //긴급 상황 발생 트리거
    void TriggerEmergency(GameObject vehicle)
    {
        VehicleControl vehicleControl = vehicle.GetComponent<VehicleControl>();
        int vehicleSegment = vehicleControl.GetSegmentVehicleIsIn();

        vehicleControl.vehicleStatus = VehicleControl.Status.Stop;
        vehicleQueue.Add(vehicle);
    }
    //빠져나갔다면 긴급 상황이 해제되었을 경우
    private void ExitEmergency(GameObject vehicle)
    {
        vehicle.GetComponent<VehicleControl>().vehicleStatus = VehicleControl.Status.Go;
    }
    //트리거 발생시 처리
    private void OnTriggerEnter(Collider other)
    {
        //차량이 이미 목록에 있는지 확인하고, 그렇다면 처리 안함
        //방금 시작한 앱이라면 처리 안함(아예 시작 시 교차로에 차량이 있는 경우)
        if(IsAlreadyInIntersection(other.gameObject) || Time.timeSinceLevelLoad < 0.5f)
        {
            return;
        }
        //차량이 아니면 무시
        if(other.tag.Equals(TrafficHeadquarter.VehicleTagLayer) == false)
        {
            return;
        }
        //교차로의 타입에 따라 처리되는 함수가 다르다.
        switch(intersectionType)
        {
            case IntersectionType.Stop:
                TriggerStop(other.gameObject);
                break;
            case IntersectionType.TrafficLight:
                TriggerLight(other.gameObject);
                break;
            case IntersectionType.Emergency:
                TriggerEmergency(other.gameObject);
                break;
        }
    }
    //트리거에서 빠져 나갔을 때
    private void OnTirggerExit(Collider other)
    {
        if(other.tag.Equals(TrafficHeadquarter.VehicleTagLayer) == false)
        {
            return;
        }

        switch(intersectionType)
        {
            case IntersectionType.Stop:
                ExitStop(other.gameObject);
                break;
            case IntersectionType.TrafficLight:
                ExitLight(other.gameObject);
                break;
            case IntersectionType.Emergency:
                ExitEmergency(other.gameObject);
                break;
        }
    }
}

 

 


 

 

신호등 에셋 설정

 

 

TrafficLightControl 스크립트 생성

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

public class TrafficLightControl : MonoBehaviour
{
    public int lightGroupID;
    public TrafficIntersection intersection;
    //라이트
    private Light pointLight;
    //긴급 상황시 깜빡임 정도
    private float blink = 0f;
    //교통 신호에 따라 색깔 변경
    void SetTrafficLightColor()
    {
        if(intersection.currentRedLightGroup == lightGroupID)
        {
            pointLight.color = Color.red;
        }
        //긴급상황일 때
        else if(intersection.currentRedLightGroup == 0)
        {
            blink = Mathf.Clamp01(blink + Time.deltaTime * 2f);
            pointLight.color = new Color(blink, 0f, 0f);
            if(blink >= 1f)
            {
                blink = 0f;
            }
        }
        else
        {
            pointLight.color = Color.green;
        }
    }

    private void Start()
    {
        pointLight = GetComponentInChildren<Light>();
        SetTrafficLightColor();
    }

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

 

 


 

 

웨이포인트 화살표 표시, 기즈모 표시

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 const string VehicleTagLayer = "AutonomousVehicle";
    //교차로 어디있는지
    public List<TrafficIntersection> intersections = new List<TrafficIntersection>();

    //에디터용 기즈모 속성들. HQ에서 조절
    public enum ArrowDraw
    {
        FixedCount,
        ByLength,
        Off
    }
    //기즈모에 그릴 화살표 속성
    public bool hideGizmos = false;
    public ArrowDraw arrowDrawType = ArrowDraw.ByLength;
    public int arrowCount = 1;
    public float arrowDistance = 5f;
    public float arrowSizeWaypoint = 1;
    public float arrowSizeIntersection = 0.5f;

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

 

 


 

 

툴 만들기

Scripts 폴더 아래에 Editor 폴더 만들어서 EditorHelper, InspectorHelper 스크립트 두 개 생성

 

 

EditorHelper 스크립트 생성

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor; //네임스페이스 넣어준다.
using System;

public static class EditorHelper
{
    public static void SetUndoGroup(string label)
    {
        //이 뒤로 나오는 모든 변화를 하나의 그룹으로 묶는다는 뜻
        //Ctrl + z 하면 그룹 단위로 취소된다.
        Undo.SetCurrentGroupName(label);
    }
    public static void BeginUndoGroup(string undoName, TrafficHeadquarter trafficHeadquarter)
    {
        //undo그룹 세팅
        Undo.SetCurrentGroupName(undoName);
        //headquarter에서 발생하는 모든 변화를 등록하게 된다.
        Undo.RegisterFullObjectHierarchyUndo(trafficHeadquarter.gameObject, undoName);
    }
    //게임 오브젝트 생성해서 트랜스폼 리셋해서 돌려줌
    public static GameObject CreateGameObject(string name, Transform parent = null)
    {
        GameObject newGameObject = new GameObject(name);
        newGameObject.transform.position = Vector3.zero;
        newGameObject.transform.localScale = Vector3.one;
        newGameObject.transform.localRotation = Quaternion.identity;

        Undo.RegisterFullObjectHierarchyUndo(newGameObject, "Spawn Create GameObject");
        Undo.SetTransformParent(newGameObject.transform, parent, "Set Parent");
        return newGameObject;
    }
    //컴포넌트 붙이는 작업도 undo가 가능하도록 세팅.
    public static T AddComponent<T>(GameObject target) where T : Component
    {
        return Undo.AddComponent<T>(target);
    }
    //직선과 구 충돌 판별식 b^2 - ac가 0보다 크거나 작거나 같을 때. true라면 구 반경에 레이가 hit 되었다는 뜻
    public static bool SphereHit(Vector3 center, float radius, Ray ray)
    {
        Vector3 originToCenter = ray.origin - center;
        float a = Vector3.Dot(ray.direction, ray.direction);
        float b = 2f * Vector3.Dot(originToCenter, ray.direction);
        float c = Vector3.Dot(originToCenter, originToCenter) - (radius * radius);
        float discriminant = b * b - 4f * a * c;
        //구에 충돌하지 않음
        if(discriminant < 0f)
        {
            return false;
        }
        //한 점 이상 충돌
        float sqrt = Mathf.Sqrt(discriminant);
        return -b - sqrt > 0f || -b + sqrt > 0f;
    }
    //없는 레이어 생성하는 함수
    public static void CreateLayer(string name)
    {
        if (string.IsNullOrEmpty(name))
        {
            throw new ArgumentException("name", "새로운 레이어를 추가하려면 이름을 꼭 입력해주세요");
        }

        var tagManager = new SerializedObject(AssetDatabase.LoadAllAssetsAtPath("ProjectSettings/TagManager.asset")[0]);
        var layerProps = tagManager.FindProperty("layers");
        var propCount = layerProps.arraySize;

        SerializedProperty firstEmptyProp = null;
        for(var i = 0; i < propCount; i++)
        {
            var layerProp = layerProps.GetArrayElementAtIndex(i);
            var stringValue = layerProp.stringValue;
            if (stringValue == name)
            {
                return;
            }
            //이미 다른 레이어가 자리를 차지하고 있다면
            if(i < 8 || stringValue != string.Empty)
            {
                continue; //넘어간다
            }
            if(firstEmptyProp == null)
            {
                firstEmptyProp = layerProp;
                break; //하나만 찾으면 돼서 break;
            }
        }
        if(firstEmptyProp == null)
        {
            Debug.LogError($"레이어가 최대 개수에 도달하였습니다. {name}을(를) 생성하지 못했습니다.");
            return;
        }
        firstEmptyProp.stringValue = name;
        tagManager.ApplyModifiedProperties();
    }
    /// <summary>
    /// GameObject에 레이어를 세팅한다. 원하면 자식들도 전부 다 세팅한다.
    /// </summary>
    /// <param name="gameObject"></param>
    /// <param name="layer"></param>
    /// <param name="includeChildren"></param>
    public static void SetLayer(this GameObject gameObject, int layer, bool includeChildren = false)
    {
        if (!includeChildren)
        {
            gameObject.layer = layer;
            return;
        }

        foreach (var child in gameObject.GetComponentsInChildren<Transform>(true)
        {
            child.gameObject.layer = layer;
        }
    }
}

 

 

InspectorHelper 스크립트 생성

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

public class InspectorHelper
{
    public static void Label(string label)
    {
        EditorGUILayout.LabelField(label);
    }
    public static void Header(string label)
    {
        EditorGUILayout.LabelField(label, EditorStyles.boldLabel);
    }
    public static void Toggle(string label, ref bool isToggle)
    {
        isToggle = EditorGUILayout.Toggle(label, isToggle);
    }
    public static void IntField(string label, ref int value)
    {
        value = EditorGUILayout.IntField(label, value);
    }
    public static void IntField(string label, ref int value, int min, int max)
    {
        value = Mathf.Clamp(EditorGUILayout.IntField(label, value), min, max);
    }
    public static void FloatField(string label, ref float value)
    {
        value = EditorGUILayout.FloatField(label, value);
    }
    public static void FloatField(string label, ref float value, float min, float max)
    {
        value = Mathf.Clamp(EditorGUILayout.FloatField(label, value), min, max);
    }
    public static void PropertyField(string label, string value, SerializedObject serializedObject)
    {
        SerializedProperty extra = serializedObject.FindProperty(value);
        EditorGUILayout.PropertyField(extra, new GUIContent(label), true);
    }
    public static void HelpBox(string content)
    {
        EditorGUILayout.HelpBox(content, MessageType.Info);
    }
    public static bool Button(string label)
    {
        return GUILayout.Button(label);
    }
    public static void DrawArrowTypeSelection(TrafficHeadquarter trafficHeadquarter)
    {
        trafficHeadquarter.arrowDrawType = (TrafficHeadquarter.ArrowDraw)EditorGUILayout.EnumPopup("화살표 타입", trafficHeadquarter.arrowDrawType);
        EditorGUI.indentLevel++;

        switch (trafficHeadquarter.arrowDrawType)
        {
            case TrafficHeadquarter.ArrowDraw.FixedCount:
                IntField("Count", ref trafficHeadquarter.arrowCount, 1, int.MaxValue);
                break;
            case TrafficHeadquarter.ArrowDraw.ByLength:
                FloatField("화살표 사이의 거리", ref trafficHeadquarter.arrowDistance);
                break;
            case TrafficHeadquarter.ArrowDraw.Off:
                break;
            default:
                throw new ArgumentOutOfRangeException();

        }
        if(trafficHeadquarter.arrowDrawType != TrafficHeadquarter.ArrowDraw.Off)
        {
            FloatField("화살표 사이즈 : 웨이포인트", ref trafficHeadquarter.arrowSizeWaypoint);
            FloatField("화살표 사이즈 : 교차로", ref trafficHeadquarter.arrowSizeIntersection);
        }
        EditorGUI.indentLevel--;
    }
}
728x90
반응형