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

2024. 2. 23. 16:41SKKU DT

728x90
반응형

타일 맵을 만들고

 

 

Segment를 [Ctrl + 좌클릭]으로 만들고 Waypoint를 [Shift + 좌클릭]으로 만든다.

Intersection은 교차로에 [Alt + 좌클릭]으로 만든 후 콜라이더를 키워서 교차로를 덮는다.

 

 

각 Segment들은 Next Segments로 갈 수 있는 다음 세그먼트를 리스트로 추가한다.

 

 

신호등에는 Traffic Light Control 스크립트를 넣고 해당하는 Intersection을 끌어놓고 신호등 규칙에 따라 [Light Group ID]를 설정한다. 대각선 위치의 신호등에 같은 ID를 부여하면 된다. ID는 0은 쓰지 않고 1과 2를 사용해야 한다. 1을 써야 하는 지 2를 써야 하는 지는 밑에 추가로 설명했다.

 

 

교차로 전체에 TRAFFIC_LIGHT 타입을 선택한다.

 

 

교차로의 스크립트에서, Light Group 1, 2에 같은 신호를 받아야 하는 세그먼트들을 추가한다. 신호를 받아야 하는 도로를 추가한다.

 

 

해당 세그먼트에 영향을 주는 신호등도 Light Group 1, Light Group 2에 맞게 Light Group ID를 부여해야 한다. 예를 들어 위 사진에서 세그먼트 19와 9가 Light Group 1의 영향을 받는다고 설정했다면, 세그먼트 19와 9에게 영향을 주는 신호등의 Light Group ID는 1이어야 한다.

 

 

그리고 차량의 Raycast Anchor 위치가 바닥에 있어서 앞 차를 인지 못하고 추돌을 일으켜서 VehicleSettingEditor 스크립트에서 레이캐스트 앵커 만드는 부분의 Raycast 위치를 new Vector로 새로 정의하였다.

//레이캐스트 앵커 만들기
GameObject anchor = EditorHelper.CreateGameObject("Raycast Anchor", selected.transform);
anchor.transform.localPosition = new Vector3(0, 0.3f, 1);
anchor.transform.localRotation = Quaternion.identity;

 

 

잘 돌아가긴 하는데 안쪽 교차로에선 좌회전, 바깥에선 우회전만한다. 바깥에서 안쪽 교차로로 좌회전해서 들어오는 차가 없는데 이유를  찾아봐야겠다.

 

 

연구해보니 세그먼트의 f두 갈래 길 중 상위 하나만 가는 걸로 되어있어서 랜덤으로 설정해야겠다.

 

 

VehicleControl 스크립트에 랜덤 설정은 되어있었지만, Random.Range()안의 값이 nextSegment.Count - 1로 설정되어 있어서 마지막 리스트 객체를 받아오지 못하고 있었다. -1을 뺐더니 이제 사거리에서도 우회전을 한다!!

다음 길이 세 갈래라면 세그먼트가 3이고, Random.Range(0, 2)가 되기 때문에 0, 1만 값이 나온다. 하지만 Random.Range(0, 3)이라고 하면 0, 1, 2가 나오므로 원하는 값이 나온 것이다.

 

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

    int randomCount = Random.Range(0, nextSegments.Count); //nextSegment.Count - 1 에서 수정
    return nextSegments[randomCount].ID;
}

 

 

 


 

 

Google SpreadSheet와 데이터 연동하기

 

링크를 복사해서 안에 데이터에 접근할 것이다.

 

 

위에 데이터 범위를 확인하고, 아래 내용으로 링크를 작성하면, 주소창에 치면 tsv 파일이 다운로드 되고 열면 내용이 들어있다.

 

docs.google.com/spreadsheets/d/1CeR3NISoN_HY_hbqCdl_q7mHdx2YvoFodcAdZ4xwySc/export?format=tsv&range=A2:B17&gid=0

 

 

 

SpreadSheetLoader 스크립트 작성

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using UnityEngine.Networking;
using System.Reflection;

public class SpreadSheetLoader : MonoBehaviour
{
    //구글 스프레드 시트를 TSV 형식으로 읽어올 수 있도록 주소를 만든다.
    public static string GetSheetDataAddress(string address, string range, long sheetID)
    {
        return $"{address}/export?format=tsv&range={range}&gid={sheetID}";
    }

    public readonly string ADDRESS = "https://docs.google.com/spreadsheets/d/1CeR3NISoN_HY_hbqCdl_q7mHdx2YvoFodcAdZ4xwySc";
    public readonly string RANGE = "A2:B17";
    public readonly long SHEET_ID = 0;
    //읽어온 스트링 데이터를 임시 저장 합니다.
    private string loadString = string.Empty;
    //구글 스프레드 시트의 TSV 얻는 주소를 이용해 데이터를 읽어옵니다.
    private IEnumerator LoadData(Action<string> onMessageReceived)
    {
        //구글 데이터 로딩 시작
        UnityWebRequest www = UnityWebRequest.Get(GetSheetDataAddress(ADDRESS, RANGE, SHEET_ID));
        yield return www.SendWebRequest();
        //데이터 로딩 완료
        Debug.Log(www.downloadHandler.text);
        if(onMessageReceived != null)
        {
            onMessageReceived(www.downloadHandler.text);
        }
        yield return null;
    }
    //
    public string StartLoader()
    {
        StartCoroutine(LoadData(output => loadString = output)); //람다식

        return loadString;
    }
}

 

 

Traffic Headquarter 오브젝트 아래에 DataLoader 빈 오브젝트 만들어서 Spread Sheet Loader 스크립트 넣기

 

 

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;
    }

    //데이터 로딩 속성들
    public class EmergencyData
    {
        public int ID = -1;
        public bool IsEmergency = false;
        public EmergencyData(string id, string emergency)
        {
            ID = int.Parse(id);
            IsEmergency = emergency.Contains("1"); //1이라는 글자만 있으면 emergency로
        }
    }
    public class TrafficData
    {
        public List<EmergencyData> datas = new List<EmergencyData>();
    }
}

 

 

데이터를 표시할 UI Text를 화면에 배치한다.

 

 

TrafficLabel 태그로 UI 텍스트를 찾을 수 있게 설정했다.

 

 

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;
    }

    //데이터 로딩 속성들
    public class EmergencyData
    {
        public int ID = -1;
        public bool IsEmergency = false;
        public EmergencyData(string id, string emergency)
        {
            ID = int.Parse(id);
            IsEmergency = emergency.Contains("1"); //1이라는 글자만 있으면 emergency로
        }
    }
    public class TrafficData
    {
        public List<EmergencyData> datas = new List<EmergencyData>();
    }
    //data 출력한 UI라벨
    public TMPro.TextMeshProUGUI stateLabel;
    //구글 스프레드 시트 읽어올 로더
    public SpreadSheetLoader dataLoader;
    //읽어온 데이터 클래스
    private TrafficData trafficData;

    private void Start()
    {
        dataLoader = GetComponentInChildren<SpreadSheetLoader>();
        stateLabel = GameObject.FindWithTag("TrafficLabel").GetComponent<TMPro.TextMeshProUGUI>();
        //일정 주기로 데이터 로딩을 시킬예정, 텀이 짧으면 URL이 막힌다.
        InvokeRepeating("CallLoaderAndCheck", 5f, 5f);
    }
    private void CallLoaderAndCheck()
    {
        string loadedData = dataLoader.StartLoader();
        stateLabel.text = "Traffic Status \n " + loadedData;
        if (string.IsNullOrEmpty(loadedData))
        {
            return;
        }
        //data를 class에 담는다.
        trafficData = new TrafficData();
        string[] AllRow = loadedData.Split('\n'); //줄바꿈
        foreach(string onerow in AllRow)
        {
            string[] datas = onerow.Split("\t"); //탭으로 쪼개기
            EmergencyData data = new EmergencyData(datas[0], datas[1]);
            trafficData.datas.Add(data);
        }
        //data 검사. 응급상황 발생시 세팅
        CheckData();
    }

    private void CheckData()
    {
        for(int i = 0; i < trafficData.datas.Count; i++)
        {
            EmergencyData data = trafficData.datas[i];
            if(intersections.Count <= 1 || intersections[i] == null)
            {
                return;
            }
            if(data.IsEmergency)
            {
                intersections[data.ID].IntersectionType = IntersectionType.EMERGENCY;
            }
            else
            {
                intersections[data.ID].IntersectionType = IntersectionType.TRAFFIC_LIGHT;
            }
        }
    }

}

 

 

 

 

 

13번 ID에 1을 넣으면 13번 교차로에 Emergency 상황으로 깜빡이는 것을 볼 수 있다.

 

 


 

 

SpreadSheetLoader 스크립트 수정 -제네릭 타입으로 사용 가능 하도록

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using UnityEngine.Networking;
using System.Reflection;

public class SpreadSheetLoader : MonoBehaviour
{
    //구글 스프레드 시트를 TSV 형식으로 읽어올 수 있도록 주소를 만든다.
    public static string GetSheetDataAddress(string address, string range, long sheetID)
    {
        return $"{address}/export?format=tsv&range={range}&gid={sheetID}";
    }

    public readonly string ADDRESS = "https://docs.google.com/spreadsheets/d/1CeR3NISoN_HY_hbqCdl_q7mHdx2YvoFodcAdZ4xwySc";
    public readonly string RANGE = "A2:B15";
    public readonly long SHEET_ID = 0;
    //읽어온 스트링 데이터를 임시 저장 합니다.
    private string loadString = string.Empty;
    //구글 스프레드 시트의 TSV 얻는 주소를 이용해 데이터를 읽어옵니다.
    private IEnumerator LoadData(Action<string> onMessageReceived)
    {
        //구글 데이터 로딩 시작
        UnityWebRequest www = UnityWebRequest.Get(GetSheetDataAddress(ADDRESS, RANGE, SHEET_ID));
        yield return www.SendWebRequest();
        //데이터 로딩 완료
        Debug.Log(www.downloadHandler.text);
        if (onMessageReceived != null)
        {
            onMessageReceived(www.downloadHandler.text);
        }
        yield return null;
    }
    //
    public string StartLoader()
    {
        StartCoroutine(LoadData(output => loadString = output)); //람다식

        return loadString;
    }

    T GetData<T>(string[] datas, string childType = "")
    {
        object data;
        if (string.IsNullOrEmpty(childType) || Type.GetType(childType) == null)
        {
            data = Activator.CreateInstance(typeof(T));
        }
        else
        {
            data = Activator.CreateInstance(Type.GetType(childType));
        }

        FieldInfo[] fieldInfos = typeof(T).GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
        for(int i = 0; i< datas.Length; i++)
        {
            try
            {
                Type type = fieldInfos[i].FieldType;

                if (string.IsNullOrEmpty(datas[i]))
                {
                    continue;
                }
                if(type == typeof(int))
                {
                    fieldInfos[i].SetValue(data, int.Parse(datas[i]));
                }
                else if(type == typeof(float))
                {
                    fieldInfos[i].SetValue(data, float.Parse(datas[i]));
                }
                else if(type == typeof(bool))
                {
                    fieldInfos[i].SetValue(data, bool.Parse(datas[i]));
                }
                else if(type == typeof(string)) //스트링은 맨 마지막에
                {
                    fieldInfos[i].SetValue(data, datas[i]);
                }
            }
            catch(Exception e)
            {
                Debug.LogError($"SpreadSheet Load Error : {e.Message}");
            } 
        }
        return (T)data;
    }
    public List<T> GetDatas<T>(string data)
    {
        List<T> returnList = new List<T>();
        string[] splitedData = data.Split('\n');
        foreach(string element in splitedData)
        {
            string[] datas = element.Split('\t');
            returnList.Add(GetData<T>(datas));
        }
        return returnList;
    }
}

 

 

TrafficHeadquarter 스크립트에 [Serializable] 한 줄 추가

using System;
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;
    }
    [Serializable]
    //데이터 로딩 속성들
    public class EmergencyData
    {
        public int ID = -1;
        public bool IsEmergency = false;
        public EmergencyData(string id, string emergency)
        {
            ID = int.Parse(id);
            IsEmergency = emergency.Contains("1"); //1이라는 글자만 있으면 emergency로
        }
    }
    public class TrafficData
    {
        public List<EmergencyData> datas = new List<EmergencyData>();
    }
    //data 출력한 UI라벨
    public TMPro.TextMeshProUGUI stateLabel;
    //구글 스프레드 시트 읽어올 로더
    public SpreadSheetLoader dataLoader;
    //읽어온 데이터 클래스
    private TrafficData trafficData;

    private void Start()
    {
        dataLoader = GetComponentInChildren<SpreadSheetLoader>();
        stateLabel = GameObject.FindWithTag("TrafficLabel").GetComponent<TMPro.TextMeshProUGUI>();
        //일정 주기로 데이터 로딩을 시킬예정, 텀이 짧으면 URL이 막힌다.
        InvokeRepeating("CallLoaderAndCheck", 5f, 5f);
    }
    private void CallLoaderAndCheck()
    {
        string loadedData = dataLoader.StartLoader();
        stateLabel.text = "Traffic Status \n " + loadedData;
        if (string.IsNullOrEmpty(loadedData))
        {
            return;
        }
        //data를 class에 담는다.
        trafficData = new TrafficData();
        string[] AllRow = loadedData.Split('\n'); //줄바꿈
        foreach(string onerow in AllRow)
        {
            string[] datas = onerow.Split("\t"); //탭으로 쪼개기
            EmergencyData data = new EmergencyData(datas[0], datas[1]);
            trafficData.datas.Add(data);
        }
        //TrafficData.datas = dataLoader.GetDatas<EmergencyData>(loadedData);
        //data 검사. 응급상황 발생시 세팅
        CheckData();
    }

    private void CheckData()
    {
        for(int i = 0; i < trafficData.datas.Count; i++)
        {
            EmergencyData data = trafficData.datas[i];
            if(intersections.Count <= 1 || intersections[i] == null)
            {
                return;
            }
            if(data.IsEmergency)
            {
                intersections[data.ID].IntersectionType = IntersectionType.EMERGENCY;
            }
            else
            {
                intersections[data.ID].IntersectionType = IntersectionType.TRAFFIC_LIGHT;
            }
        }
    }
}

 

 

좌회전 + 직진 충돌 빼고는 완성!!

 

 


 

 

길찾기 -A* Pathfinding

https://arongranberg.com/astar/

 

A* Pathfinding Project

Cult of the Lamb Start your own cult in a land of false prophets, venturing out into diverse and mysterious regions to build a loyal community of woodland Followers and spread your Word to become the one true cult. We love the A* Pathfinding Project, Aron

arongranberg.com

 

https://assetstore.unity.com/packages/tools/behavior-ai/a-pathfinding-project-pro-87744?aid=1011l7JId&utm_campaign=unity_affiliate&utm_medium=affiliate&utm_source=partnerize-linkmaker

 

A* Pathfinding Project Pro | 행동 AI | Unity Asset Store

Get the A* Pathfinding Project Pro package from Aron Granberg and speed up your game development process. Find this & other 행동 AI options on the Unity Asset Store.

assetstore.unity.com

 

 


 

 

간단하게 사운드 만들 수 있는 사이트

https://sfxr.me/

 

jsfxr

8-bit game sound effects generator in JavaScript.

sfxr.me

728x90
반응형