결과를 보면 하나는 시계방향 하나는 반시계 방향으로 도는 것을 확인할 수 있다. Unity는 왼손 좌표계이기 때문에 왼손 법칙을 사용하여 X축으로 90도 회전하면 Z축은 땅방향을 가리키게 된다.(Y축은 X축의 영향을 받지 않는다.) Y축은 원래 하늘 방향이었기 때문에 두 축에 대한 회전은 같은 면에서 발생하지만 방향이 반대로 나타나게 된다.
이와 같은 결과는 어떤한 회전 종속성을 가지고 있더라도 중간에 있는 축을 회전하면 축이 겹치는 짐벌락 현상이 발생할 수 있다.
* 대안
yaw, roll 짐벌 사이의 큰 각도를 유지하기 위해 모터에 의해 능동적으로 돌아가는 4번째 짐벌을 사용하여 극복할 수 있다.
짐벌락이 감지되었을 때 하나 이상의 짐벌을 임의의 위치로 회전시키고 리셋시킨다. (축 재정렬)
오일러 각은 실제로 인스펙터에 표시되는 x, y, z로 표시되며 이를 통해 씬에서 회전할 수 있다. 하지만 Unity에서는 GameObject의 실제 회전이나 방향에 대한 정보를 내부적으로 쿼터니언으로 저장한다. 이렇게 쿼터니언으로 저장하는 이유는 짐벌락이 발생할 수 있는 복잡한 동작을 표현하는데 유용하기 때문이다.
- 오일러 각(Euler angle)
Transform에서 Unity는 벡터 속성 Transform.eulerAngles의 X, Y, Z로 회전을 표시한다. 이 값들은 각각 X, Y, Z축에 대한 회전을 의미한다.
오일러 각은 축들에 대한 개별 회전을 수행하며 Z => X => Y 순서로 순차적으로 적용된다. 회전의 방식은 외부 회전이기 때문에 회전하는 동안 원래 좌표계는 변경되지 않는다.
계산과 회전을 위해 오일러 각으로 변환하여 사용할 수 있지만 짐벌락이 발생할 위험이 있다.
- 짐벌락(Gimbal lock)
3차원 공간의 한 물체가 자유도를 잃고 2차원 안에서만 회전할 수 있을 때 이를 짐벌락이라고 부른다.
// 여기서 자유도란 독립적으로 달라질 수 있는 시스템의 매개 변수이다.
짐벌락은 오일러 각에서 두 축이 평행해지면 발생한다. 만약 오일러 각으로 변환시키지 않고 쿼터니언을 사용한다면 짐벌락을 방지할 수 있다.
만약 오일러 각을 사용하고 있다면 회전을 위해 Transform.RotateAround을 사용하고 각 축에 대해서 Quaternion.AngleAxis를 적용하고 곱해서 사용할 수 있다.
- 쿼터니언(Quaternion)
쿼터니언은 3D 공간에서 공간적인 방향이나 회전을 나타내기 위한 수학적 표기법을 제공한다. 쿼터니언은 4개의 숫자를 사용하여 3D 에서 단위 축과 회전 방향과 각도를 나타낸다.
Unity에서는 회전값을 쿼터니언 형식으로 변환하여 저장하고 있는데 이는 쿼터니언 회전이 효율적이고 계산에 안정적이기 때문이다. 한 쿼터니언은 어떤 축에 대한 360도 보다 큰 값을 나타낼 수 없기 때문에 에디터에서는 회전을 쿼터니언으로 나타내지 않는다.
오일러 각과 쿼터니언 간 변환은 다음과 같이 수행할 수 있다.
오일러 각 => 쿼터니언 : Quaternion.Euler 함수 사용
쿼터니언 => 오일러 각 : Quaternion.eulerAngles 함수 사용
스크립트에서 회전을 다룰 때 회전 값을 생성, 수정하기 위해 Quaternion 클래스를 사용해야 한다. 오일러 각을 사용해야하는 상황에서는 오일러 각과 관련된 Quaternion 클래스를 사용한다. 만약 회전에서 오일러 값을 검색, 수정, 재적용과 같은 작업을 수행하면 짐벌락과 같은 부작용이 발생할 수 있다.
// 검색은 별 이상 없을 것 같지만 Quaternion.eulerAngles을 수행하면 내부 쿼터니언 값을 오일러 각으로 변환시킨다. 현재 회전을 오일러 각을 통해 나타내는 방식은 여러 가지가 존재하기 때문에 설정한 값과 다르게 나올 수 있다.
// 되도록 쿼터니언을 통해 회전을 표현하도록 노력해보고 만약 오일러 각을 사용해야하는 경우라면 오일러 각을 검색할 때 원하지 않는 값이 나올 수 있기 때문에 검색한 값을 사용하지말고 따로 변수로 저장하여 관리하여 회전을 표현하는 것이 좋다
GameObject는 씬에 있는 것들을 표현하고 있는 클래스이다. GameObject의 특성이나 기능은 Component 구성에 따라서 결정되며 GameObject는 기능적인 Component들의 컨테이너로 동작한다.
생성하려는 GameObject의 종류에 따라 Component들의 조합을 다르게 하여야 한다. GameObject는 항상 Transform Component를 가지고 있으며 이를 삭제하지 못한다.
스크립팅에서 찾기, 연결, GameObject 간 메시지 전달, Component 추가 / 삭제와 같은 기능을 지원하고 있다. GameObject.SetActive 함수를 통해 GameObject의 활성 상태를 컨트롤할 수 있으며 만약 비활성화되었다면 일반적으로 보이지 않고, Component들도 비활성화되고 Update나 FixedUpdate와 같은 일반적인 이벤트를 받을 수 없다.
* MonoBehaviour
MonoBehaviour는 Unity 스크립트가 파생되는 가장 기본적인 클래스이다. 스크립트를 생성하면 기본적으로 이 클래스를 상속받고 있다.
MonoBehaviour는 GameObject에 스크립트를 붙일 수 있도록 해주고 Start와 Update와 같은 이벤트 함수들도 제공하고 있다. MonoBehavior는 Component을 상속 받고 있다. 코루틴 관련한 기능(시작, 중지, 관리)들도 제공하고 있는데 이러한 코루틴은 비동기 코드를 작성할 수 있는 방법으로 사용된다. // 코루틴은 멀티스레드가 아니다. 멀티스레드는 Unity의 잡 시스템이다.
MonoBehaviour는 이벤트 메시지 컬렉션에 접근을 제공하고 있기 때문에 이벤트 기반으로 코드를 수행할 수 있다.
이벤트 메시지 종류의 예시는 다음과 같다.
Start : GameObject가 존재하기 시작했을 때 호출된다. 그리고 씬이 로드되거나 GameObject가 인스턴스화 됐을 때 호출된다.
Update : 매 프레임 호출된다.
FixedUpdate : 매 물리 타임 스텝에 맞춰 호출된다.
OnBecameVisible , OnBecameInvisible : GameObject의 Renderer가 카메라 안에 들어오거나 나갔을 때 호출된다.
OnCollisionEnter , OnTriggerEnter : 물리적인 충돌이나 트리거가 발생했을 때 호출한다.
OnDestroy - GameObject가 파괴되었을 때 호출된다.
* Transform
Transform은 Component의 한 종류로 GameObject의 위치, 회전, 스케일 정보를 다루는 다양한 방법을 제공한다. 또한 GameObject의 부모 자식간 계층적 관계로 작업하는 여러 방법을 제공한다. GameObject는 항상 Transform Component를 가지고 있고 제거할 수 없다.
계층적 관계에 있는 부모, 자식 GameObject가 존재한다고 하면 자식 GameObject는 부모의 이동, 회전, 스케일링에 영향을 받는다. 인스펙터에서는 자식 GameObject의 Transform 값은 부모 GameObject의 Transform 값에 상대적으로 표시된다. Transform은 로컬 좌표와 월드 좌표를 둘다 제공할 수 있으며 변환도 가능하다.
* 개인 지식 기반과 여러 곳에서 읽은 것들을 기반으로 작성하였습니다. 틀린 부분이 많을 수 있습니다. 틀린 부분이 있으면 알려주시면 감사하겠습니다.
* 상속 기반 설계와 컴포넌트 기반 설계
언리얼 엔진에서는 객체 지향형으로 설계되어 있고(상속 기반), 유니티에서는 컴포넌트 기반으로 설계되어 있다.
- 클래스 예시
예를 언리얼 엔진에서는 Pawn를 컨트롤할 수 있는 Actor 종류, Character을 인간형 Pawn으로 계층적으로 정의하고 있다. 하지만 유니티에서는 GameObject라는 Entity(기본 빈 객체)만 있고 그 아래에 컴포넌트를 붙여나가는 식으로 프로그래밍하도록 되어있다.
- 프로그래밍 예시
어떤 액션 게임에서 검, 활, 총 등과 같은 여러 가지 무기를 설계하는 과정을 생각해보자.
언리얼 엔진에서는 어떤 클래스를 만들 때 어떤 클래스를 상속 받아서 작성할지 자세히 지정할 수 있게 되어있다. 따라서 Actor를 상속하여 원거리 무기 클래스와 근거리 무기 클래스로 나누고 검은 근거리 무기 클래스를 상속받고, 활과 총은 원거리 무기를 상속받아 작성할 수 있을 것이다. 잘 짜여진 계층구조를 설계해야한다.
유니티에서는 클래스를 만들 때 기본적으로 MonoBehaviour라는 컴포넌트를 상속받은 클래스가 생성된다. 무기를 구성하는데 필요한 여러 컴포넌트를 설계한다. 공격력 컴포넌트, 공격범위 컴포넌트, 발사체 컴포넌트 등의 컴포넌트를 설계하고 무기의 특징에 따라 컴포넌트를 조합하여 구성한다.
- 장단점 비교
객체 지향형 설계
컴포넌트 기반 설계
장점
@ 컴포넌트와 달리 하나로 되어있기 때문에 그 안에서 모두 처리할 수 있다.
@ 계층적 구조를 잘 나타낼 수 있기 때문에 구조를 쉽게 파악할 수 있다.
@ 코드 수정이 쉽다. // 위의 예시에서 발사체에 대한 처리를 고치려면 발사체 컴포넌트 하나를 수정하면 된다.
@ 코드 유지 보수가 쉽고, 확장이 유연하다.
단점
@ 관리해야할 클래스가 많아질수록 계층구조가 더욱 복잡해지고 이를 유지 관리하는데 어려움을 겪거나 시간이 오래걸릴 수 있다.
@ 코드 수정, 추가 과정이 복잡할 수 있다. // 위의 예시에서발사체에 대한 처리를 고치려면 계층 구조를 확인하고 적절한 곳에 수정해야 한다. 계층 구조를 다시 설계해야할 수도 있다.
@ 여러 컴포넌트가 복합적으로 사용되어야 하는 구조에서 컴포넌트 간 복잡한 상호작용이 필요하다.
First, they provide only a single form of primitive interconnection - method invocation. This makes it difficult to represent richer types of component interaction as first class design elements.
Second, they have weak support for hierarchical description, making it difficult to describe systems at increasing levels of detail.
Third, they do not support the definition of families of systems. While they can be used to describe patterns and to define a vocabulary of object types, they don't have explicit syntactic support for characterizing a class of system in terms of the design constraints that each member of the family must observe.
Fourth, they do not provide direct support for characterizing and analyzing non-functional properties. This makes it difficult to reason about critical system design properties, such as system performance and reliability
UCLASS()
class AMyActor : public AActor
{
GENERATED_BODY()
// My trigger component
UPROPERTY()
UPrimitiveComponent* Trigger;
AMyActor()
{
Trigger = CreateDefaultSubobject<USphereComponent>(TEXT("TriggerCollider"));
// Both colliders need to have this set to true for events to fire
Trigger.bGenerateOverlapEvents = true;
// Set the collision mode for the collider
// This mode will only enable the collider for raycasts, sweeps, and overlaps
Trigger.SetCollisionEnabled(ECollisionEnabled::QueryOnly);
}
virtual void NotifyActorBeginOverlap(AActor* Other) override;
virtual void NotifyActorEndOverlap(AActor* Other) override;
};
* 입력
Unity
public class MyPlayerController : MonoBehaviour
{
void Update()
{
if (Input.GetButtonDown("Fire"))
{
// ...
}
float Horiz = Input.GetAxis("Horizontal");
float Vert = Input.GetAxis("Vertical");
// ...
}
}
// Find GameObject by name
GameObject MyGO = GameObject.Find("MyNamedGameObject");
// Find Objects by type
MyComponent[] Components = Object.FindObjectsOfType(typeof(MyComponent)) as MyComponent[];
foreach (MyComponent Component in Components)
{
// ...
}
// Find GameObjects by tag
GameObject[] GameObjects = GameObject.FindGameObjectsWithTag("MyTag");
foreach (GameObject GO in GameObjects)
{
// ...
}
// Find Actor by name (also works on UObjects)
AActor* MyActor = FindObject<AActor>(nullptr, TEXT("MyNamedActor"));
// Find Actors by type (needs a UWorld object)
for (TActorIterator<AMyActor> It(GetWorld()); It; ++It)
{
AMyActor* MyActor = *It;
// ...
}
UnrealEngine
// Find UObjects by type
for (TObjectIterator<UMyObject> It; It; ++It)
{
UMyObject* MyObject = *It;
// ...
}
// Find Actors by tag (also works on ActorComponents, use TObjectIterator instead)
for (TActorIterator<AActor> It(GetWorld()); It; ++It)
{
AActor* Actor = *It;
if (Actor->ActorHasTag(FName(TEXT("Mytag"))))
{
// ...
}
}