벡터의 내적
벡터의 내적은 게임 수학에서 자주 사용되는 개념이다.
내적의 결과는 스칼라 값이다.
이 결과를 통해 다음과 같은 정보를 알 수 있다.
- 결과가 음수라면 두 벡터의 사이각이 90˚~270˚ 사이에 있다.
- 내적은 $cos \theta$ 가 곱해지기 때문이다
- 90˚~270˚에 있다는 뜻은 자신보다 뒤에 있다는 뜻이 된다.
- 내적의 결과는 하나의 벡터를 다른 벡터에 투영한 벡터의 크기와 같다.
- 즉, 하나의 벡터를 분리하여 다른 벡터의 방향에 대한 크기를 알 수 있다.
간단한 예로, 입사각과 반사각을 생각해 보자.
바닥이 고정되어 있고 하나의 물체가 특정한 방향으로 떨어져 바닥과 완전 탄성 충돌을 한다고 가정해 보자.
그렇다면 물체가 바닥에 튕겨 나갈 것이다.
이를 수학적으로 계산하면 다음과 같다.
바닥은 고정되어 있어 normal이 up벡터로 고정되어 있을 것이다.
그렇다면 물체가 떨어져 바닥과 충돌하는 입사각과 up벡터 간 내적의 결과는 파란 벡터의 크기와 같다.
이를 충돌한 바닥의 normal에 한 번 더해주면 바닥과 평행한 벡터를 구할 수 있다.
평행한 벡터는 충돌하는 물체의 이동 벡터에서 바닥의 normal 벡터 성준을 제거한 벡터 방향으로 생겨난다.
추가적으로 한 번 더 더해주면 완전 탄성 충돌의 결과처럼 반사되도록 만들 수 있다.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CustomBoxCollider : MonoBehaviour
{
public Vector3 direction;
public float Speed;
void Update()
{
transform.position += direction * (Speed * Time.deltaTime);
}
private void OnTriggerEnter(Collider other)
{
var dotedVector = Vector3.Dot(direction, other.transform.up);
//슬라이딩
direction += other.transform.up * -dotedVector;
//완전 탄성 충돌
direction += other.transform.up * -dotedVector * 2;
}
}
궤적 그리기
발사체의 포물선 궤적을 그리는 방법을 알아보자.
포물선을 그리는 방법은 실제 물체가 포물선 운동할 위치를 미리 계산하여 일정한 간격으로 preview를 그려주는 것이다.
List<Vector3> PredictTrajectory(Vector3 force, float mass)
{
List<Vector3> trajectories = new List<Vector3>();
Vector3 position = transform.position;
Vector3 velocity = force / mass;
trajectories.Add(position);
for (int i = 1; i <= maxStep; i++)
{
float timeElapsed = timeStep * i;
// 등가속도 운동
trajectories.Add(position +
velocity * timeElapsed +
Physics.gravity * (0.5f * timeElapsed * timeElapsed));
if (CheckCollision(trajectories[i - 1], trajectories[i], out Vector3 hitPoint))
{
trajectories[i] = hitPoint;
break;
}
}
return trajectories;
}
등가속도 운동을 한다고 가정하고 일정 시간이 지난 후의 위치를 저장한 List를 반환하는 함수이다.
CheckCollision은 충돌이 일어난 이후로는 preview가 그려질 필요가 없기 때문에 다음을 그리지 않는 것이다.
이 함수로 전달받은 포지션에 preview object를 생성해주면 된다.
if (Input.GetKeyDown(KeyCode.Z))
{
List<Vector3> trajectorys = PredictTrajectory(transform.forward * Power, Mass);
foreach (var o in Objects)
{
Destroy(o);
}
Objects.Clear();
foreach (var trajectory in trajectorys)
{
var go = Instantiate(Trajectory, trajectory, Quaternion.identity);
Objects.Add(go);
}
}
전체 코드는 다음과 같다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Cannon : MonoBehaviour
{
public float Power = 500.0f;
public float Mass = 10.0f;
public int maxStep = 20;
public float timeStep = 0.1f;
public GameObject CannonBall;
public GameObject Trajectory;
public List<GameObject> Objects = new List<GameObject>();
List<Vector3> PredictTrajectory(Vector3 force, float mass)
{
List<Vector3> trajectories = new List<Vector3>();
Vector3 position = transform.position;
Vector3 velocity = force / mass;
trajectories.Add(position);
for (int i = 1; i <= maxStep; i++)
{
float timeElapsed = timeStep * i;
// 등가속도 운동
trajectories.Add(position +
velocity * timeElapsed +
Physics.gravity * (0.5f * timeElapsed * timeElapsed));
if (CheckCollision(trajectories[i - 1], trajectories[i], out Vector3 hitPoint))
{
trajectories[i] = hitPoint;
break;
}
}
return trajectories;
}
private bool CheckCollision(Vector3 start, Vector3 end, out Vector3 hitPoint)
{
hitPoint = end;
Vector3 direction = end - start;
float distance = direction.magnitude;
if (Physics.Raycast(start, direction.normalized, out RaycastHit hit, distance, 1 << LayerMask.NameToLayer("Default")))
{
hitPoint = hit.point;
return true;
}
return false;
}
// Update is called once per frame
void Update()
{
if (Input.GetKey(KeyCode.W))
{
transform.rotation *= Quaternion.Euler(-90 *Time.deltaTime, 0,0);
}
if (Input.GetKey(KeyCode.S))
{
transform.rotation *= Quaternion.Euler(90*Time.deltaTime,0,0);
}
if (Input.GetKeyDown(KeyCode.Space))
{
GameObject go = Instantiate(CannonBall, transform.position, transform.rotation);
go.GetComponent<Rigidbody>().mass = Mass;
go.GetComponent<Rigidbody>().AddForce(transform.forward * Power, ForceMode.Impulse);
Destroy(go, 3.0f);
}
if (Input.GetKeyDown(KeyCode.Z))
{
List<Vector3> trajectorys = PredictTrajectory(transform.forward * Power, Mass);
foreach (var o in Objects)
{
Destroy(o);
}
Objects.Clear();
foreach (var trajectory in trajectorys)
{
var go = Instantiate(Trajectory, trajectory, Quaternion.identity);
Objects.Add(go);
}
}
foreach (var o in Objects)
{
o.SetActive(false);
}
List<Vector3> trajectorys2 = PredictTrajectory(transform.forward * Power, Mass);
if (Objects.Count == trajectorys2.Count)
{
for (var index = 0; index < trajectorys2.Count; index++)
{
var trajectory = trajectorys2[index];
Objects[index].SetActive(true);
Objects[index].transform.position = trajectory;
}
}
}
}
Object에 UI 붙이기
Object에 체력바같은 UI를 붙이고 싶을 수 있다.
Object에 UI를 바로 붙이게 되면 Object안에 Canvas가 생겨나면서 붙게 된다.
하지만, 모든 객체에 Canvas를 붙이게 되면 DrawCall이 폭발적으로 증가할 수 있다.
모든 Canvas를 합쳐 하나의 화면으로 그려내기 때문에 Canvas를 많이 생성하게 되면 성능상에 문제가 생긴다.
따라서, 하나의 Canvas를 부모로 UI를 동적으로 생성하여 붙인 뒤, 자신의 owner를 저장한 채로 update로 position을 지정하면 된다.
여기서 궁금증이 생겼다.
Prefab에 이미 만들어진 UI들은 화면에 Render 할 수 없을까?
테스트해 보니 Canvas가 부모가 되지 않으면 화면에 그려지지 않는다.
아마도 UI안에 CanvasRender에서 Canvas를 찾지 못해 그러는 것 같다.
그래서 UI의 부모를 Canvas로 바꿔주고 위치를 조정했더니 화면에 나타났다.
즉, UI가 화면에 그려지려면 다음과 같은 조건을 만족해야 한다.
- UI의 부모 중, Canvas가 있어야 한다.
- UI가 Canvas안에 있어야 한다.
현재 게임은 카메라가 게임 카메라, UI 카메라로 분리되어 있어 두 카메라 간의 위치 변환이 필요하다.
UI가 붙을 캐릭터(몬스터)의 위치는 게임 카메라를 기준으로 하는 World Position이기 때문이다.
void LateUpdate()
{
if (owner != null && camera != null)
{
Vector3 screenPoint = camera.WorldToScreenPoint(owner.position);
Vector2 localPoint;
RectTransformUtility.ScreenPointToLocalPointInRectangle(
transform.parent.GetComponent<RectTransform>(), screenPoint, ui_camera, out localPoint);
transform.localPosition = localPoint;
}
}