게임수학(맛보기)
벡터
벡터 : '크기'와 '방향'을 가지는 값
좁은 의미(유클리드 기하학)에서 말하는 의미라고 하지만, 게임 수학에서만 사용할 것이므로 패스 !
벡터의 덧셈
각 요소들을 더해줌
Ex) <3,5> + <2,-1> = <5,4>
순서가 바뀌어도 상관 없다.
벡터의 뺄셈
각 요소들을 빼줌
Ex) < 6,7 > - <1,3> = <5,4>
만약 반대로 빼 준다면, magnitude는 같지만, 방향은 다른 벡터가 생성된다.
<1,3> - <6,7> = <-5,-4>
* magnitude (length , 2D는 root of (x+x+y+y), 3D는 root of (x+x+y+y+z+z) 로 구한다. )
벡터의 곱셈
각 요소들에 곱해지는 숫자를 따로따로 곱해줌
Ex) <1,2> x 3 = <3,6>
나눗셈은 반대이다.
곱셈과 나눗셈을 해도 힘의 방향은 바뀌지 않음, 이 성징르 이용해 노멀라이즈를 함
벡터를 자신의 크기로 나눈다. 이렇게 해서 나온 값을 노말라이즈 벡터라고 함.
힘의 방향은 일정하지만 강도를 제어할 수 있는 경우에 유용하다.
벡터의 연산에서 나오는 스칼라 값들
스칼라 값은 '크기'만을 가지는 값
내적과 외적
벡터의 내적(Dot Product)
두 벡터를 받아 스칼라를 반환함. 두 벡터의 닮은 정도 라고 보면 편함.
두 벡터가 얼마나 같은 방향으로 향하는지를숫자로 나타낸 것,
Cos(세타)가 작을수록 값이 커지고, 힘의 크기에 따라 무한히 커질 수 있음
공식은 다음과 같고, dot 이라고 유니티 연산에 있음.
벡터의 내적이 0이면 두 벡터의 방향이 90도로 직교하고 있는 것이고
0보다 크다면 같은 방향을 , 0보다 작다면 다른 방향을 향하고 있는 것.
내적을 이용해서 몬스터의 사람 감지 같은 기능을 구현할 수 있음
간단하게 내적의 쓰임새를 설명하자면 ,
캐릭터가 앞(Forward를 보고 있다면, 시선 방향과 물체로 향하는 벡터 값이 양수라면 시야 범위 내에, 아니라면 밖에 있다고 볼 수 있다.
시야각 내 물체 판별도 할 수 있다. 다음과 같은 코드를 보자.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MyCharacter : MonoBehaviour
{
public Transform child;
public Transform target;
public float checkDegree;
public float checkDistance;
void OnDrawGizmos()
{
Vector3 direction = child.position - transform.position;
direction.Normalize();
Vector3 directionToTarget = target.transform.position - transform.position;
float distance = directionToTarget.magnitude;
directionToTarget.Normalize();
float dot = Vector3.Dot(direction, directionToTarget);
float angle = Mathf.Cos(checkDegree * 0.5f * Mathf.Deg2Rad);
if (distance > checkDistance)
{
Gizmos.color = Color.red;
}
else if (dot > angle)
{
Gizmos.color = Color.blue;
}
else
{
Gizmos.color = Color.red;
}
Debug.Log($"{angle} {dot}");
for (float i = -checkDegree / 2; i < checkDegree / 2; ++i)
{
Vector3 dir = Quaternion.Euler(0, i, 0) * direction * checkDistance;
Gizmos.DrawLine(transform.position, transform.position + dir);
}
}
}
우선, 위에서 벡터의 뺄셈을 한 방식으로 시선 벡터와, 물체까지의 벡터를 구한 뒤 노말라이즈 시켜준다.
이를 통해 우리는 값이 1인, 방향 단위 벡터를 얻었다.
이 벡터를 내적시켜준다. 이 내적시켜준 값은 시선 벡터와 물체까지의 벡터의 cos값과 같다.
이유는 벡터의 내적 공식에 있다.
V1 V2의 내적은, V1,V2의 크기 곱하기 Cos(세타)이다.
여기서 V1,V2의 크기가 노말라이즈된 1이라면, V1,V2의 내적은 cos(세타)가 된다.
그럼 여기까지는, 두 벡터의 내적을 구했고, 이는 cos(세타), 즉 두 벡터 사이의 각도의 코사인값이었다.
그리고 이 값이 dot에 저장되어 있다.
이제 시야각의 cos값을 구해보자.
시야각은 양쪽으로 펼쳐져 있으니 1/2을 해주고 ,유니티는 삼각함수를 사용할 때 radian 값을 사용하니 바꿔준다.
그리고 MathF.Cos 값으로 바꿔주면 Cos값이 나온다.
이 값은 시선부터 한쪽 시야각까지의 Cos값이다.
이 두 Cos값을 비교해서 시야각 안에 (반대쪽 시야각도 똑같이 적용되니) 물체가 있는지 판별할 수 있다.
Cos값은 크면 클 수록 방향이 비슷한 것이다.
따라서
Dot > angle 이면, 시야각보다 물체가 안에 있는 것이고, 반대면 밖에 있는 것이다.
시야각 안에, checkdistace보다 가까이 있다면 기즈모 컬러를 blue로 바꿔준다.
이제 기즈모를 그릴 차례이다.
Quaternion.Euler( 0, i, 0) * directon * checkDistance; 는
- checkDegree/2부터 checkdegree까지 1도씩 회전하는 것에 벡터와 거리를 곱한 것이며,
벡터에 회전을 곱해주면 벡터가 그 방향으로 회전하게 된다.
그리고 선을 그려주면 끝이다. !
벡터의 외적 (Cross Product)
두 벡터가 만나서 새로운 벡터를 만드는 계산
원래 두 벡터와 수직인 방향을 가리킴
벡터의 크기는 다음과 같음.
아직 외적으로 무언가를 구현하거나, 자세히 알아보지 않았다.
참조:
https://docs.unity3d.com/kr/530/Manual/UnderstandingVectorArithmetic.html
벡터 연산 이해 - Unity 매뉴얼
벡터 연산은 3D 그래픽스, 물리 연산 및 애니메이션에 있어 핵심적이며 Unity를 최대한 활용하기 위해서는 벡터 연산을 깊이 이해하는 것이 유용합니다. 여기서는 주 연산 및 유용하게 사용할 수
docs.unity3d.com
https://www.youtube.com/watch?v=2aNkZjGeonA&t=382s
제 2 Cos법칙
Cos인 제 2법칙으로 우리는 두 변과 그 변 사이의 각도를 알면, 나머지 한 변의 크기를 알 수 있게 되었다.
어디에 쓰이는지는 아직 모른다ㅎㅎ..
입사각, 반사각, 법선
법선 : 곡선상의 한 점이나, 곡면상의 한 점을 지나고 이 점의 점평면에 수직인 직선
입사각 : 광선이 입사점에서 경계면의 법선과 이루는 각
반사각 : 빛이 반사한 점의 물질의 경계면에서의 법선과 반사 광선이 이루는 각, 입사각과 동일
법선을 알면, 물체의 이동을 튕겨나가게 하거나, 벽면에 슬라이딩(벽타기) 시킬 수 있다.
Unity에는 Reflect라는 함수로 구할 수 있다.
매개변수로는 입사 벡터 값, 법사 벡터 값을 받는다.
우리가 입사 벡터는 쉽게 구할 수 있지만 ( 물체를 이동시키면서 자연스럽게) 법선 벡터는 아니다.
법선 벡터를 구하는 방법은 크게 3가지가 있다.
- 직접 표면의 normal 정보를 입력하는 방법 : 한 쪽 평면의 normal 값만 필요한 경우, 표면이 여러개면 X, 오브젝트마다 입력 필요로 자주 사용되지 않음 (노말 벡터는 평면에 수직인 벡터)
- 충돌 처리를 위한 OnCollision Enter, Stay , Exit에 GetContact() 함수가 있음.
GetContact는 n번째 접촉점을 나타냄, 0을 입력해 첫번째 접촉점을 받아올 수 있고 .normal을 사용해
입사 벡터를 얻을 수 있다. (의문 : 이렇게 얻어내는 값은 벡터 값의 크기가 1인가.. ?) - Raycast를 사용해 광선을 쏘고, 부딫힌 물체의 .normal을 사용해 값을 얻을 수 있다.
https://www.youtube.com/watch?v=uJ9teYEqJ48
https://docs.unity3d.com/kr/2019.4/Manual/ComputingNormalPerpendicularVector.html
노멀/수직 벡터 컴퓨팅 - Unity 매뉴얼
노멀 벡터(즉, 평면에 수직인 벡터)는 메시 생성 중에 자주 필요하며 경로 추적 및 다른 상황에서도 유용합니다. 평면의 세 점(예: 메시 삼각형의 코너 점)이 주어질 경우, 노멀을 쉽게 찾을 수 있
docs.unity3d.com
투사체 궤적
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> trajectory = new List<Vector3>();
Vector3 position = transform.position;
Vector3 velocity = force / mass;
trajectory.Add(position);
for (int i = 1; i <= maxStep; i++)
{
float timeElapsed = timeStep * i;
// 등가속도 운동
trajectory.Add(position +
velocity * timeElapsed +
Physics.gravity * (0.5f * timeElapsed * timeElapsed));
if (CheckCollision(trajectory[i - 1], trajectory[i], out Vector3 hitPoint))
{
trajectory[i] = hitPoint;
break;
}
}
return trajectory;
}
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.Z))
{
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.Space))
{
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;
}
}
}
}
투사체를 발사하고, 그 궤적을 그리며 충돌시 사라지게 하는 기능이다.
자세한 설명보다는, 몰랐던 것을 위주로 분석해보겠다.
Vector3 velocity = force / mass;
이는 유니티에서 Impulse를 구하는 공식이다.
PredictTrajectory라는 함수는, Vector3 force와 mass(질량)을 인자로 받는다.
새로운 투사체 리스트를 만들고, 포지션과 속도를 새로운 변수에 할당한다.
리스트에 포지션을 저장한다.
그리고 i = 1 부터 maxStep(표시할 궤적을 나타내는 물체의 수)까지 time step의 속도로 리스트에 포지션을 추가해주는데
등가속도 운동의 공식을 이용한다.
동작 중에, i-1번째 trajectory와 i 번째 trajectory 사이에 충돌이 있다면, 그 값을 hitpoin로 반환하고 중단한다.
checkCollision은 Raycast를 사용해서 만든다.
List<Vector3> PredictTrajectory(Vector3 force, float mass)
{
List<Vector3> trajectory = new List<Vector3>();
Vector3 position = transform.position;
Vector3 velocity = force / mass;
trajectory.Add(position);
for (int i = 1; i <= maxStep; i++)
{
float timeElapsed = timeStep * i;
// 등가속도 운동
trajectory.Add(position +
velocity * timeElapsed +
Physics.gravity * (0.5f * timeElapsed * timeElapsed));
if (CheckCollision(trajectory[i - 1], trajectory[i], out Vector3 hitPoint))
{
trajectory[i] = hitPoint;
break;
}
}
return trajectory;
}
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;
}
지금까지가 이 부분이다.
다음 부분은 배웠던 것이다.
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.Z))
{
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);
}
위 아래로 움직이는 발사 포인트를 만들고, Z키를 누르면 캐논볼을 생성하고 발사한다. ( 질량을 할당한)
* 위에서 velocity를 impulse 공식으로 했으니 impulse로 힘을 가해준다.
궤적을 미리 표시하는 코드는 다음 부분이다.
if (Input.GetKeyDown(KeyCode.Space))
{
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);
}
}
스페이스 키를 누르면, 위에서 만들어두었던 함수로 trajectorys라는 배열을 생성한다.
그리고 이전에 만들었을 Ojbects 배열 안에 있는 모든 오브젝트를 삭제해주고 리스트도 비운다.
foreach문에서 trajectorys 배열 안에 있는 trajectory Vector3 값들을 하나 하나 꺼내서
그 위치에 Trajectory 프리팹을 생성하고, Ojbect리스트에 넣는다. ( 대문자 소문자 s에 주의)
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;
}
}
마지막 부분이다. Update()문 안에 들어있음을 상기하고 바라본다면 왜 이런 코드가 나왔는지 알 수 있다.
궤적은 방향을 바꾸면 계속해서 바뀐다. 이를 표시해주기 위해 Update문 안에
Objects 리스트에 있는 모든 오브젝트를 끄고
새로운 trajectorys2 배열을 똑같은 계산으로 만들어주고,
Objects 배열.count가 새로 만든 배열의 count와 같다면 새롭게 위치를 저장하고,
모든 오브젝트를 켜 준 뒤 위치도 최신화 시켜준다.
이는 화면에 표시되는 궤적을 최신화 하기 위함이다.
매 번 생성/파괴를 반복하지 않기에 조금 더 리소스 측면에서 효율적인 방식이다.
의문 1 : 궤도 오브젝트의 개수가 달라진다면 ? 궤도가 사라진 상태(false)로 지속되나 ?
맞다. 다시 스페이스 바를 누를때까지 변경되지 않는다.
(비활성화된 오브젝트는 여전히 남아있다.)
궤적 개수가 줄어들 때, 비활성화된 오브젝트를 삭제한다거나, 자동 업데이트를 한다거나 등의 업그레이드 방안은 남아있지만, 우리가 만들 앵그리버드를 위해 남겨놓도록 하자.
알아보고 싶은 키워드
- Interface
- 드로우콜, 배칭, 인스턴시, 아틀라싱
- 키네마틱
- HashTable
- Impulse 외 다른 forcemode들의 공식
HP바를 캔버스에 표시하는 것은 주말에 따로 한 번 해봐야지.
'TIL' 카테고리의 다른 글
abstract, Interface (1) | 2024.12.21 |
---|---|
[멋쟁이사자처럼 부트캠프 TIL회고] Unity 게임 개발 3기 31일차 (0) | 2024.12.20 |
[멋쟁이사자처럼 부트캠프 TIL회고] Unity 게임 개발 3기 29일차 (2) | 2024.12.18 |
[멋쟁이사자처럼 부트캠프 TIL회고] Unity 게임개발 3기 28일차 (1) | 2024.12.17 |
[멋쟁이사자처럼 부트캠프 TIL회고] Unity 게임 개발 3기 27일차 (1) | 2024.12.17 |