2024. 2. 20. 18:36ㆍSKKU DT
백터의 내적(dot)
Cube 생성 - 기본위치
sphere 생성 - (0, 0, -3)
TestScript 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
public class TestScript : MonoBehaviour
{
public TextMeshProUGUI textLabel;
public Transform target;
// Update is called once per frame
void Update()
{
// 타겟 없이는 동작하지 않습니다. (실무에서도 꼭 주석을 달아준다.)
if (target == null)
{
return;
}
// cube transform
Vector3 lhs = transform.forward;
// target 으로 향하는 젝터, 크기를 노멀라이즈해서 방향만 얻습니다.
Vector3 rhs = (target.position - transform.position).normalized;
// 내적을 구합니다. 최대 1, 최소 -1.
float dot = Mathf.Clamp(Vector3.Dot(lhs, rhs), -1, 1);
// 타겟 포지션으로부터의 역벡터를 구합니다.
Vector3 lineVector = transform.InverseTransformPoint(target.position);
// 레이를 그려봅니다.타겟으로 향하는 레이, 큐브의 forward를 나타내는 레이.
Debug.DrawRay(transform.position, lineVector, Color.red);
Debug.DrawRay(transform.position, transform.position, Color.cyan);
// 텍스트로 내적의 값을 출력합니다.
// .ToString("F1")은 소수점 한자리까지 표현한다는 뜻.
textLabel.text = dot.ToString();
}
}
실행시키면 아래와 같이 나온다.
게임뷰에서는 아래 화면처럼 나온다.
내적이 0보다 크면 나보다 앞, 내적이 0보다 작으면 나보다 뒤를 의미한다.
교통 시뮬레이션(Traffic Simulation) 만들기
강사님이 제공한 패키지 import (resource_first.unitypackage)
씬 하나 만들어서 차량 올려놓기,
기존에 있던 [Mesh Renderer] 등 컴포넌트 다 지우고 바퀴 4개에 각각 [Wheel Collider] 추가, 차체에는 [Rigidbody] 추가
차체 [Rigidbody] 설정은 아래와 같다.
[Wheel Collider] 설정 값은 아래와 같다.
*참고 [Wheel Collider] 프로퍼티
차체에 [Box Collider] 추가해서 [is Trigger] 켜기
빈 자식 오브젝트 2개 만들어서 [Box Collider] 추가, 크기는 조정해준다.
WheelDriveControl 스크립트 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.IO;
[RequireComponent(typeof(Rigidbody))] //Rigidbody를 삭제할 수 없다. 누군가 실수로 Rigidbody를 뺄 수 없다.
public class WheelDriveControl : MonoBehaviour
{
public enum DriveType
{
RearWheelDrive, //후륜구동
FrontWheelDrive, //전륜구동
AllWheelDrive //4륜구동
}
public enum SpeedUnitType
{
KMH,
MPH
}
[Tooltip("차량에 적용되는 다운포스")]
public float downForce = 100f;
[Tooltip("바퀴의 최대 조향 각도")]
public float maxAngle = 60f;
[Tooltip("조향 각도각에 도달하는 속도(선형 보간)")]
public float steeringLerp = 5f;
[Tooltip("차량이 방향을 바꾸려고 할 때의 최대 속도")]
public float steeringSpeedMax = 8f;
[Tooltip("구동 바퀴에 적용되는 최대 토크(힘)")]
public float maxTorque = 100f;
[Tooltip("구동 바퀴에 적용되는 최대 브레이크 토크")]
public float brakeTorque = 100000f;
[Tooltip("속도 단위")]
public SpeedUnitType unitType = SpeedUnitType.KMH;
[Tooltip("최소 속도 - 주행 시 (정지 / 브레이크 제외) 단위는 선택한 속도 단위, 반드시 0보다 커야 합니다.")]
public float minSpeed = 2f;
[Tooltip("위에서 선택한 단위의 최대 속도")]
public float maxSpeed = 10f;
[Tooltip("바퀴는 휠 콜라이더의 자식으로 따로 붙여줍니다. 링크 필요.")]
public GameObject leftWheelShape;
public GameObject rightWheelShape;
[Tooltip("바퀴에 애니메이션 효과를 적용할지의 여부")]
public bool animateWheels = true;
[Tooltip("차량의 구동 유형 : 후륜, 전륜, 4륜")]
public DriveType driveType = DriveType.RearWheelDrive;
private WheelCollider[] wheels;
private float currentSteering = 0f;
private Rigidbody rb;
public void Init()
{
rb = GetComponent<Rigidbody>();
wheels = GetComponentsInChildren<WheelCollider>(); //하위에 있는 컴포넌트 가져오기
for(int i = 0; i < wheels.Length; i++)
{
var wheel = wheels[i];
//필요할 때만 바퀴 모양을 만들 예정
if(leftWheelShape != null && wheel.transform.localPosition.x < 0)
{
var wheelShape = Instantiate(leftWheelShape);
wheelShape.transform.parent = wheel.transform;
}
else if(rightWheelShape != null && wheel.transform.localPosition.x > 0)
{
var wheelShape = Instantiate(rightWheelShape);
wheelShape.transform.parent = wheel.transform;
}
wheel.ConfigureVehicleSubsteps(10, 1, 1); //초기 세팅 (시작할 때 얼마나 큰 힘이 필요한가, 얼마나 줄어들 것인가, )
}
}
private void Awake()
{
Init();
}
private void OnEnable()
{
Init();
}
//현재 속도를 단위에 맞추기
public float GetSpeedMS(float speed)
{
if(speed == 0f)
{
return 0f;
}
return unitType == SpeedUnitType.KMH ? speed / 3.6f : speed / 2.237f;
}
public float GetSpeedUnit(float speed)
{
return unitType == SpeedUnitType.KMH ? speed * 3.6f : speed * 2.237f;
}
//이동하면서 바퀴의 조향 기능도 있고 Rigidbody에 힘을 가해주는 기능으로 이동한다.
public void Move(float _acceleration, float _steering, float _brake)
{
float nSteering = Mathf.Lerp(currentSteering, _steering, Time.deltaTime * steeringLerp); //현재, 원하는 스티어, 타임*러프 값
currentSteering = nSteering;
if(rb == null)
{
rb = GetComponent<Rigidbody>();
}
float angle = maxAngle * nSteering;
float torque = maxTorque * _acceleration;
float handbrake = _brake > 0f ? brakeTorque : 0f;
foreach(var wheel in wheels)
{
//앞바퀴 조향
if (wheel.transform.localPosition.z > 0)
{
wheel.steerAngle = angle;
}
// 뒷바퀴 조향
if(wheel.transform.localPosition.z < 0)
{
wheel.brakeTorque = handbrake;
}
// 전륜이 아니면 뒷바퀴에 토크를 줘라
if(wheel.transform.localPosition.z < 0 && driveType != DriveType.FrontWheelDrive)
{
wheel.motorTorque = torque;
}
// 후륜이 아니면 앞바퀴에 토크를 줘라
if(wheel.transform.localPosition.z > 0 && driveType != DriveType.RearWheelDrive)
{
wheel.motorTorque = torque;
}
//휠 트랜스폼 정보를 위에 세팅한 값에 따라 변경함으로써 애니메이션 효과. 애니메이터 없이 코드로 돌리기
if(animateWheels)
{
Quaternion rotation;
Vector3 pos;
wheel.GetWorldPose(out pos, out rotation);
Transform shapeTransform = wheel.transform.GetChild(0); //첫번째 자식
shapeTransform.position = pos;
shapeTransform.rotation = rotation;
}
}
if(rb != null)
{
//가속을 준다.
float speedUnit = GetSpeedUnit(rb.velocity.magnitude);
if(speedUnit > maxSpeed)
{
rb.velocity = GetSpeedMS(maxSpeed) * rb.velocity.normalized;
}
//downforce를 부여한다.
rb.AddForce(-transform.up * downForce * rb.velocity.magnitude); //포스 모드를 설정 안하면 기본 설정으로 된다.
}
}
}
*Tooltip은 아래 사진과 같이 마우스를 변수명에 올렸을 때 설명이 나온다. 툴팁은 주석 역할도 할 수 있다.
VehicleControl 스크립트 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
public class VehicleControl : MonoBehaviour
{
private WheelDriveControl wheelDriveControl;
private float initMaxSpeed = 0f;
private void Start()
{
wheelDriveControl = GetComponent<WheelDriveControl>();
initMaxSpeed = wheelDriveControl.maxSpeed;
}
private void Update()
{
float acceleration = 1f;
float brake = 0f;
float steering = 0f;
wheelDriveControl.maxSpeed = initMaxSpeed;
wheelDriveControl.Move(acceleration, steering, brake);
}
}
비어있는 컴포넌트 추가하면 차량이 움직인다.
3인칭 카메라 설정
CameraControl 스크립트 생성
*Camera는 LateUpdate를 사용한다. 캐릭터가 다 움직이고 나서 업데이트 되어야 하기 때문.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CameraControl : MonoBehaviour
{
private Transform myTransform = null;
//타겟으로부터 떨어진 거리
public float distance = 5f;
//타겟으로부터의 높이
public float height = 1.5f;
//높이값 변경 속도
public float heightDamping = 2.0f;
//회전값 변경 속도
public float rotationDamping = 3.0f;
//타겟
public Transform target = null;
private void Start()
{
myTransform = GetComponent<Transform>();
//타겟이 없다면 Player라는 태그를 가지고 있는 게임오브젝트가 타겟이다.
if(target == null)
{
target = GameObject.FindWithTag("Player").transform;
}
}
private void LateUpdate()
{
if(target == null)
{
return;
}
//카메라가 목표로 하고 있는 회전 Y축값과 높이값
float wantedRotationAngle = target.eulerAngles.y;
float wantedHeight = target.position.y + height;
//현재 카메라가 바라보고 있는 회전 Y축값과 높이값
float currentRotationAngle = myTransform.eulerAngles.y;
float currentHeight = myTransform.position.y;
//현재 카메라가 바라보고 있는 회전값과 높이값을 보간해서 새로운 값으로 계산
currentRotationAngle = Mathf.LerpAngle(currentRotationAngle, wantedRotationAngle, rotationDamping * Time.deltaTime);
currentHeight = Mathf.Lerp(currentHeight, wantedHeight, heightDamping * Time.deltaTime);
//위에서 계산한 회전값으로 쿼터니언 회전값을 생성
Quaternion currentRotation = Quaternion.Euler(0.0f, currentRotationAngle, 0.0f);
//카메라가 타겟의 위치에서 회전하고자 하는 벡터만큼 뒤로 물러난다.
myTransform.position = target.position;
myTransform.position -= currentRotation * Vector3.forward * distance;
//이동한 위치에서 원하는 높이값으로 올라간다.
myTransform.position = new Vector3(myTransform.position.x, currentHeight, myTransform.position.z);
//타겟을 항상 바라보도록 한다. forward -> target
myTransform.LookAt(target);
}
}
위의 코드에서 좌우 키로 자동차가 회전을 하게 만들 수 있다.
float steering = Input.GetAxisRaw("Horizontal");
TrafficHeadquarter 스크립트 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TrafficHeadquarter : MonoBehaviour
{
//세그먼트와 세그먼트 사이의 검출 간격
public float segDetectThresh = 0.1f;
//웨이포인트의 크기
public float waypointSize = 0.5f;
//충돌 레이어들
public string[] collisionLayers;
}
TrafficSegment 스크립트 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TrafficSegment : MonoBehaviour
{
//다음에 이동할 segment들
public List<TrafficSegment> nextSegments = new List<TrafficSegment>();
//이 세그먼트의 ID 값
public int ID = -1;
//구간이 갖고있는 웨이포인트들 시작 -> 끝점 등 2~3개를 보통 가지고 있다.
public List<TrafficWaypoint> waypoints = new List<TrafficWaypoint>();
public bool IsOnSegment(Vector3 pos)
{
TrafficHeadquarter trafficHeadquarter = GetComponentInParent<TrafficHeadquarter>(); //부모것 가져오기
for(int i = 0; i < waypoints.Count - 1; i++)
{
Vector3 pos1 = waypoints[i].transform.position;
Vector3 pos2 = waypoints[i + 1].transform.position;
//첫 번째 웨이포인트와 차량의 거리
float d1 = Vector3.Distance(pos1, pos);
//두 번째 웨이포인트와 차량의 거리
float d2 = Vector3.Distance(pos2, pos);
//첫 번째 웨이포인트와 두 번째 웨이포인트의 거리
float d3 = Vector3.Distance(pos1, pos2);
float diff = (d1 + d2) - d3;
//값 사이에 있다면
if(diff < trafficHeadquarter.segDetectThresh && diff > -trafficHeadquarter.segDetectThresh)
{
//자동차가 두 웨이포인트 사이에 가까이 있다.
return true;
}
}
//자동차가 두 웨이포인트 사이에서 멀리 있다. for문 끝나고 false
return false;
}
}
TrafficWaypoint 스크립트 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TrafficWaypoint : MonoBehaviour
{
public TrafficSegment segment;
//웨이포인트 중간에 있을 수 있는 콜라이더와 차량의 충돌 방지
public void RemoveCollider()
{
if (GetComponent<SphereCollider>())
{
Debug.Log("Remove Collider");
DestroyImmediate(gameObject.GetComponent<SphereCollider>());
}
}
public void Refresh(int newID, TrafficSegment newSegment)
{
segment = newSegment;
//WayPoint-1, WayPoint-10
name = "WayPoint-" + newID.ToString();
tag = "WayPoint";
gameObject.layer = LayerMask.NameToLayer("Dafault");
RemoveCollider();
}
public Vector3 GetVisualPos()
{
return transform.position + new Vector3(0.0f, 0.5f, 0.0f);
}
}
TrafficHeadquarter 스크립트 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TrafficHeadquarter : MonoBehaviour
{
//세그먼트와 세그먼트 사이의 검출 간격
public float segDetectThresh = 0.1f;
//웨이포인트의 크기
public float waypointSize = 0.5f;
//충돌 레이어들
public string[] collisionLayers;
public List<TrafficSegment> segments = new List<TrafficSegment>();
public TrafficSegment curSegment;
public List<TrafficWaypoint> GetAllWaypoints()
{
List<TrafficWaypoint> waypoints = new List<TrafficWaypoint>();
foreach (var segment in segments)
{
waypoints.AddRange(segment.waypoints);
}
return waypoints;
}
}
차 앞에 충돌 체크를 할 Raycast 추가할 예정. 차에 Anchor를 하나 붙인다.
VehicleControl 스크립트 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using Unity.PlasticSCM.Editor.WebApi;
public class VehicleControl : MonoBehaviour
{
private WheelDriveControl wheelDriveControl;
private float initMaxSpeed = 0f;
//자동차가 이동할 타겟 구조체
public struct Target
{
public int segment;
public int waypoint;
}
//자동차의 상태
public enum Status
{
Go,
Stop,
SlowDown
}
[Header("교통 관제 시스템")]
[Tooltip("현재 활성화 된 교통 시스템")]
public TrafficHeadquarter trafficHeadquarter;
[Tooltip("차량이 목표에 도달하는 시기를 확인한다. 다음 웨이포인트를 더 일찍 예상하는데 사용할 수 있습니다. (이 숫자가 높을 수록 더 빨리 예상됩니다.")]
public float waypointThresh = 2.5f;
[Header("차의 감지레이더")]
[Tooltip("레이를 쏠 앵커")]
public Transform raycastAnchor;
[Tooltip("레이의 길이")]
public float raycastLength = 3f;
[Tooltip("레이 사이의 간격")]
public float raycastSpacing = 3f;
[Tooltip("생성될 레이의 수")]
public int raycastNumber = 8;
[Tooltip("감지된 차량이 이 거리 미만이면 차가 정지")]
public float emergencyBrakeThresh = 1.5f;
[Tooltip("이 거리보다 낮거나 거리보다 높을 경우 자동차의 속도가 느려짐")]
public float slowDownThresh = 5f;
public Status vehicleStatus = Status.Go;
private int pastTargetSegment = -1;
private Target currentTarget;
private Target nextTarget;
private void Start()
{
wheelDriveControl = GetComponent<WheelDriveControl>();
initMaxSpeed = wheelDriveControl.maxSpeed;
//없으면 찾아 넣어준다.
if (raycastAnchor == null && transform.Find("Raycast Anchor") != null)
{
raycastAnchor = transform.Find("Raycast Anchor");
}
}
private void Update()
{
//테스트 코드 주석처리
//float acceleration = 1f;
//float brake = 0f;
//float steering = Input.GetAxisRaw("Horizontal");
//wheelDriveControl.maxSpeed = initMaxSpeed;
//wheelDriveControl.Move(acceleration, steering, brake);
if(trafficHeadquarter == null)
{
return;
}
}
int GetNextSegmentID()
{
//hq가 들고있는 구간 중에 현재 차량이 속해있는 세그먼트가 갖고있는 다음 구간들을 얻어온다.
List<TrafficSegment> nextSegments = trafficHeadquarter.segments[currentTarget.segment].nextSegments;
if(nextSegments.Count == 0 )
{
return 0;
}
int randomCount = UnityEngine.Random.Range(0, nextSegments.Count - 1);
return nextSegments[randomCount].ID;
}
void SetWaypointVehicleIsOn()
{
foreach (var segment in trafficHeadquarter.segments)
{
//현재 차가 이 구간에 있는지 확인
if (segment.IsOnSegment(transform.position))
{
currentTarget.segment = segment.ID;
//구간 내에서 시작할 가장 가까운 웨이포인트 찾기
float minDist = float.MaxValue;
List<TrafficWaypoint> waypoints = trafficHeadquarter.segments[currentTarget.segment].waypoints;
for(int j = 0; j < waypoints.Count; j++)
{
float distance = Vector3.Distance(transform.position, waypoints[j].transform.position);
Vector3 lSpace = transform.InverseTransformPoint(waypoints[j].transform.position);
if(distance < minDist && lSpace.z > 0f)
{
minDist = distance;
currentTarget.waypoint = j;
}
}
break;
}
}
//다음 target 찾기
nextTarget.waypoint = currentTarget.waypoint + 1;
nextTarget.segment = currentTarget.segment;
//위에 지정한 다음 타겟의 waypoint가 범위를 벗어났다면 다시 처음 0번 째 waypoint. 다음 세그먼트 아이디를 구한다.
if(nextTarget.waypoint >= trafficHeadquarter.segments[currentTarget.segment].waypoints.Count)
{
nextTarget.waypoint = 0;
nextTarget.segment = GetNextSegmentID();
}
}
}
'SKKU DT' 카테고리의 다른 글
[SKKU DT] 77일차 -교통 시뮬레이터(Traffic Simulator) 만들기(3) (0) | 2024.02.22 |
---|---|
[SKKU DT] 76일차 -교통 시뮬레이터(Traffic Simulator) 만들기(2) (0) | 2024.02.21 |
[SKKU DT] 74일차 -C#, 유니티 팁 정리 (0) | 2024.02.19 |
[SKKU DT] 73일차 -MySQL 데이터베이스, FastAPI (1) | 2024.02.16 |
[SKKU DT] 72일차 -웹 스크래핑(웹 크롤링)(3) Selenium 인스타그램 크롤링, 유튜브 크롤링 / MySQL 데이터베이스 (2) | 2024.02.15 |