2024. 2. 22. 21:06ㆍSKKU DT
어제 만들었던 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]을 눌러 차량을 세팅하면 웨이포인트를 따라 차가 움직이는 것을 볼 수 있다.
'SKKU DT' 카테고리의 다른 글
[SKKU DT] 78일차 -교통 시뮬레이터(Traffic Simulator) 만들기(4) 완성 (1) | 2024.02.23 |
---|---|
[SKKU DT] 76일차 -교통 시뮬레이터(Traffic Simulator) 만들기(2) (0) | 2024.02.21 |
[SKKU DT] 75일차 -교통 시뮬레이터(Traffic Simulator) 만들기 (0) | 2024.02.20 |
[SKKU DT] 74일차 -C#, 유니티 팁 정리 (0) | 2024.02.19 |
[SKKU DT] 73일차 -MySQL 데이터베이스, FastAPI (1) | 2024.02.16 |