목표: 3D 카메라로 바라보는 3D 오브젝트의 위치에 UI 카메라 상의 캔버스에 이미지를 표시 >> 사용 이유: 내가 선호하는 구조가 3D 카메라와 UI카메라의 복합적인 구조
using System;
using UnityEngine;
public static class ToolCamera
{
public static Vector3 ConvertPosition3DTo2DLocal(this Camera _camera3D,
Camera _camera2D, Canvas _canvas, Vector3 _position3D)
{
// :: 3D 카메라에서의 3D 위치를 3D 카메라에서의 2D 위치로 변경
Vector3 position2Din3D = _camera3D.WorldToScreenPoint(_position3D);
// :: 3D 카메라에서의 2D 위치를 UI(2D) 카메라에서의 2D 로컬 위치로 변경
Vector2 result;
RectTransformUtility.ScreenPointToLocalPointInRectangle(
_canvas.transform as RectTransform, position2Din3D, _camera2D, out result);
// :: Z축은 0으로 고정
return new Vector3(result.x, result.y, 0);
}
}
"Scripts/MonoBehaviours" 폴더에 "CameraSingleton.cs"이라는 이름의 새 C# 스크립트 파일을 만들고 다음 내용을 저장합니다:
// 메인 카메라에 접근하는 방법은 여러 가지가 있지만,
// 싱글톤(여기서 사용하는 방식)을 사용하는 접근 방식은 모든 종류의 MonoBehaviour에서 작동됩니다.
class CameraSingleton : UnityEngine.MonoBehaviour
{
public static UnityEngine.Camera Instance;
void Awake()
{
Instance = GetComponent<UnityEngine.Camera>();
}
}
"SampleScene"의 "Main Camera" 게임 오브젝트에 "CameraSingleton" MonoBehavior를 추가합니다.
"Scripts/Systems" 폴더에 "CameraSystem.cs"이라는 이름의 새 C# 스크립트 파일을 만들고 다음 내용을 저장합니다:
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
// 이 시스템은 변환 시스템이 업데이트된 후 실행되어야 합니다.
// 그렇지 않으면 카메라가 탱크보다 한 프레임 뒤로 처져 흔들림이 발생합니다.
[UpdateInGroup(typeof(LateSimulationSystemGroup))]
partial class CameraSystem : SystemBase
{
Entity Target;
Random Random;
EntityQuery TanksQuery;
protected override void OnCreate()
{
Random = Random.CreateFromIndex(1234);
TanksQuery = GetEntityQuery(typeof(Tank));
RequireForUpdate(TanksQuery);
}
protected override void OnUpdate()
{
if (Target == Entity.Null || UnityEngine.Input.GetKeyDown(UnityEngine.KeyCode.Space))
{
var tanks = TanksQuery.ToEntityArray(Allocator.Temp);
Target = tanks[Random.NextInt(tanks.Length)];
}
var cameraTransform = CameraSingleton.Instance.transform;
var tankTransform = GetComponent<LocalToWorld>(Target);
cameraTransform.position = tankTransform.Position - 10.0f * tankTransform.Forward + new float3(0.0f, 5.0f, 0.0f);
cameraTransform.LookAt(tankTransform.Position, new float3(0.0f, 1.0f, 0.0f));
}
}
플레이 모드로 들어가서 게임 뷰(이전과 같이 씬 뷰가 아님)를 보면 카메라가 탱크 중 하나를 따라가고 있음을 확인할 수 있습니다.게임 뷰에 입력 포커스가 되어 있는지 확인하고(클릭해서) 스페이스바를 반복적으로 누릅니다. 카메라는 그 때마다 매번 다른 임의의 탱크로 전환되어야 합니다.
"Scripts/Components" 폴더에 "Shooting.cs"이라는 이름의 새 C# 스크립트 파일을 만들고 다음 내용을 저장합니다:
using Unity.Entities;
// 이 태그 컴퍼넌트는 "사용 가능한 컴퍼넌트"이기도 합니다.
// 이러한 컴퍼넌트들은 엔티티에 존재하는 동안 온/오프 될 수 있습니다.
// 이렇게 하는 것이 컴퍼넌트를 추가하거나 제거하는 것보다 훨씬 효율적입니다.
struct Shooting : IComponentData, IEnableableComponent
{
}
"Scripts/Authoring" 폴더에 있는 "TurretAuthoring.cs" 파일의 내용을 다음과 같이 수정합니다:
using Unity.Entities;
class TurretAuthoring : UnityEngine.MonoBehaviour
{
public UnityEngine.GameObject CannonBallPrefab;
public UnityEngine.Transform CannonBallSpawn;
}
class TurretBaker : Baker<TurretAuthoring>
{
public override void Bake(TurretAuthoring authoring)
{
AddComponent(new Turret
{
CannonBallPrefab = GetEntity(authoring.CannonBallPrefab),
CannonBallSpawn = GetEntity(authoring.CannonBallSpawn)
});
+ // 활성화된 컴퍼넌트들은 항상 초기에 활성화됩니다.
+ AddComponent<Shooting>();
}
}
"Scripts/Systems" 폴더에 "SafeZoneSystem.cs"이라는 이름의 새 C# 스크립트 파일을 만들고 다음 내용을 저장합니다.
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
// Turret type을 처리없이 사용해야 합니다(Execute 메서드에 포함되지 않음).
[WithAll(typeof(Turret))]
[BurstCompile]
partial struct SafeZoneJob : IJobEntity
{
// 이 작업을 병렬로 실행할 경우, 서로 다른 스레드에서 동일한 엔티티에 액세스하면 문제가 발생하여
// 세이프티 시스템에서 TurretActiveFromEntity와의 잠재적인 경합 상태에 대해 항의합니다.
// 그러나 이 작업의 코드는 현재 처리 중인 엔티티만 TurretActiveFromEntity에서 조회되도록 작성되어
// 이 프로세스를 안전하게 만듭니다.
// 그래서 우리는 병렬 세이프티 체크를 비활성화할 수 있습니다.
[NativeDisableParallelForRestriction] public ComponentLookup<Shooting> TurretActiveFromEntity;
public float SquaredRadius;
void Execute(Entity entity, TransformAspect transform)
{
// 슈팅 태그 컴퍼넌트는 탱크가 지정된 범위를 벗어나는 경우에만 활성화됩니다.
TurretActiveFromEntity.SetComponentEnabled(entity, math.lengthsq(transform.Position) > SquaredRadius);
}
}
[BurstCompile]
partial struct SafeZoneSystem : ISystem
{
// ComponentLookup 랜덤 접근자는 즉석에서 생성해서는 안 됩니다.
// EntityQuery처럼, 한 번 만들고 필드에 저장해야 합니다.
ComponentLookup<Shooting> m_TurretActiveFromEntity;
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<Config>();
m_TurretActiveFromEntity = state.GetComponentLookup<Shooting>();
}
[BurstCompile]
public void OnDestroy(ref SystemState state)
{
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
float radius = SystemAPI.GetSingleton<Config>().SafeZoneRadius;
const float debugRenderStepInDegrees = 20;
// Debug rendering (the white circle).
for (float angle = 0; angle < 360; angle += debugRenderStepInDegrees)
{
var a = float3.zero;
var b = float3.zero;
math.sincos(math.radians(angle), out a.x, out a.z);
math.sincos(math.radians(angle + debugRenderStepInDegrees), out b.x, out b.z);
UnityEngine.Debug.DrawLine(a * radius, b * radius);
}
m_TurretActiveFromEntity.Update(ref state);
var safeZoneJob = new SafeZoneJob
{
TurretActiveFromEntity = m_TurretActiveFromEntity,
SquaredRadius = radius * radius
};
safeZoneJob.ScheduleParallel();
}
}
다음과 같이 "Scripts/Systems" 폴더에 있는 "TurretShootingSystem.cs" 파일의 내용을 수정합니다:
+// 슈팅 태그 컴퍼넌트의 요구로 인해 안전 영역에 있는 탱크에 대해 이 작업이 실행되는 것을 사실상 방지합니다.
+[WithAll(typeof(Shooting))]
[BurstCompile]
partial struct TurretShoot : IJobEntity
{
[ReadOnly] public ComponentLookup<LocalToWorldTransform> LocalToWorldTransformFromEntity;
public EntityCommandBuffer ECB;
void Execute(in TurretAspect turret)
{
var instance = ECB.Instantiate(turret.CannonBallPrefab);
var spawnLocalToWorld = LocalToWorldTransformFromEntity[turret.CannonBallSpawn];
var cannonBallTransform = UniformScaleTransform.FromPosition(spawnLocalToWorld.Value.Position);
cannonBallTransform.Scale = LocalToWorldTransformFromEntity[turret.CannonBallPrefab].Value.Scale;
ECB.SetComponent(instance, new LocalToWorldTransform
{
Value = cannonBallTransform
});
ECB.SetComponent(instance, new CannonBall
{
Speed = spawnLocalToWorld.Value.Forward() * 20.0f
});
// 아래 라인은 포탑에서 대포알까지 색을 전파합니다.
ECB.SetComponent(instance, new URPMaterialPropertyBaseColor { Value = turret.Color });
}
}
플레이 모드로 들어가면 탱크가 세이프 존 밖으로 나온 후에만 사격합니다. 보기 옵션에서 gizmos가 활성화 되었는지 확인하세요, 그렇지 않으면 흰색 원이 표시되지 않습니다.
여전히 플레이 모드에서, 제작된 "Config" GameObject를 선택하고 "Safe Zone Radius"를 수정합니다. "Live Baking" 덕분에 변경 사항이 실시간으로 반영됩니다.
Step 7 - 색이 입혀진 탱크들과 대포알들(Colored tanks and cannon balls)
고급 베이킹, 베이킹 시스템들에 대해 소개
ECS 컴퍼넌트들은 렌더링에 사용되는 쉐이더들에 대한 입력들을 제어할 수 있습니다. 자체 쉐이더를 만들고(쉐이더 그래프를 통해) 사용자 지정 ECS 컴퍼넌트를 입력에 맵핑하는 것은 이 튜토리얼의 범위를 벗어나지만, URPMaterialPropertyBaseColor라는 기존 컴퍼넌트를 사용할 예정입니니다. 이름에서 알 수 있듯이 표준 URP 머티리얼의 기본 색상을 제어할 수 있습니다.
우리의 탱크는 3개의 원시 요소(탱크, 터렛, 대포)로 구성되어 있으며, 각 엔티티에는 URPMaterialPropertyBaseColorcomponent가 추가되어야 합니다.
그렇게 하려면 Tank 프리팹을 열고 Hierarchy(탱크, 포탑, 대포)에서 SpawnPoint 트랜스폼을 제외하고 세 가지 기본 요소를 모두 선택하합니다. 렌더러가 없기 때문에 색상은 필요하지 않습니다.
세 가지 기본 요소를 선택한 상태에서 인스펙터의 "컴퍼넌트 추가" 단추를 사용하여 URPMaterialPropertyBaseColorAuthoringcomponent를 추가합니다.
플레이 모드로 전환하면 탱크가 완전히 검은색이 됩니다(방금 추가한 컴퍼넌트 요소의 기본값이 0,0,0,0입니다.).
플레이 모드에서 나가세요.
📝 NOTE
아래 시스템에서 사용하는 EntityCommandBuffer에는 SetComponentForLinkedEntityGroup에서 대상으로 지정할 엔티티를 지정하는 쿼리가 필요합니다. 엔티티 쿼리의 핵심은 컴퍼넌트 타입 집합들로 구성되며, 쿼리는 해당 집합과 일치하는 엔티티에 대한 필터링된 보기만 제공합니다. 엔티티 쿼리에 대한 자세한 내용은 package documentation를 참조하세요.
다음과 같이 "Scripts/Systems" 폴더에 있는 "TankSpawningSystem.cs" 파일의 내용을 수정합니다.
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Rendering;
[BurstCompile]
partial struct TankSpawningSystem : ISystem
{
+ // 쿼리는 OnUpdate의 즉석에서 만들어져서는 안 되므로 필드에 캐시됩니다.
+ EntityQuery m_BaseColorQuery;
[BurstCompile]
public void OnCreate(ref SystemState state)
{
+ // Config(구성) 싱글톤이 로드되기 전에는 이 시스템을 실행하면 안 됩니다.
+ state.RequireForUpdate<Config>();
+ m_BaseColorQuery = state.GetEntityQuery(ComponentType.ReadOnly<URPMaterialPropertyBaseColor>());
}
[BurstCompile]
public void OnDestroy(ref SystemState state)
{
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var config = SystemAPI.GetSingleton<Config>();
+ // 이 시스템은 한 번만 실행되므로 랜덤 시드를 하드 코딩할 수 있습니다.
+ // 임의 상수 시드를 사용하면 동작이 결정론적으로 진행됩니다.
+ var random = Random.CreateFromIndex(1234);
+ var hue = random.NextFloat();
+ // 가능한 한 서로 다른 색상을 만듭니다.
+ // 이 접근법의 논리는 다음 주소에서 자세히 설명합니다:
+ // https://martin.ankerl.com/2009/12/09/how-to-create-random-colors-programmatically/
+ URPMaterialPropertyBaseColor RandomColor()
+ {
+ // Note: 이 개념에 익숙하지 않은 경우, 이것은 "로컬 함수"입니다.
+ // 더 많은 정보를 얻기 위해 인터넷에서 그 용어를 검색할 수 있습니다.
+ // 0.618034005f == 2 / (math.sqrt(5) + 1) == 황금비율의 반대
+ hue = (hue + 0.618034005f) % 1;
+ var color = UnityEngine.Color.HSVToRGB(hue, 1.0f, 1.0f);
+ return new URPMaterialPropertyBaseColor { Value = (UnityEngine.Vector4)color };
+ }
var ecbSingleton = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);
var vehicles = CollectionHelper.CreateNativeArray<Entity>(config.TankCount, Allocator.Temp);
ecb.Instantiate(config.TankPrefab, vehicles);
+ // EntityQueryMask는 EntityQuery에 의해 특정 엔티티가 선택 되었는지 여부를 효율적인 테스트를 제공합니다.
+ var queryMask = m_BaseColorQuery.GetEntityQueryMask();
+ foreach (var vehicle in vehicles)
+ {
+ // 모든 프리팹의 근본에는 해당 엔티티의 모든 목록인 LinkedEntityGroup이 포함됩니다.
+ ecb.SetComponentForLinkedEntityGroup(vehicle, queryMask, RandomColor());
+ }
state.Enabled = false;
}
}
플레이 모드로 들어가면 탱크들이 무작위 색상으로 지정됩니다.
플레이 모드에서 나가세요.
"Scripts/Authoring" 폴더에 있는 "CannonBallAuthoring.cs" 파일의 내용을 다음과 같이 수정합니다:
using Unity.Entities;
using Unity.Rendering;
class CannonBallAuthoring : UnityEngine.MonoBehaviour
{
}
class CannonBallBaker : Baker<CannonBallAuthoring>
{
public override void Bake(CannonBallAuthoring authoring)
{
AddComponent<CannonBall>();
+ AddComponent<URPMaterialPropertyBaseColor>();
}
}
다음과 같이 "Scripts/Aspects" 폴더에 있는 "TurretAspect.cs" 파일의 내용을 수정합니다:
using Unity.Entities;
using Unity.Mathematics;
using Unity.Rendering;
readonly partial struct TurretAspect : IAspect
{
readonly RefRO<Turret> m_Turret;
+ readonly RefRO<URPMaterialPropertyBaseColor> m_BaseColor;
public Entity CannonBallSpawn => m_Turret.ValueRO.CannonBallSpawn;
public Entity CannonBallPrefab => m_Turret.ValueRO.CannonBallPrefab;
+ public float4 Color => m_BaseColor.ValueRO.Value;
}
다음과 같이 "Scripts/Systems" 폴더에 있는 "TurretShootingSystem.cs" 파일의 내용을 수정합니다:
[BurstCompile]
partial struct TurretShoot : IJobEntity
{
[ReadOnly] public ComponentLookup<LocalToWorldTransform> LocalToWorldTransformFromEntity;
public EntityCommandBuffer ECB;
void Execute(in TurretAspect turret)
{
var instance = ECB.Instantiate(turret.CannonBallPrefab);
var spawnLocalToWorld = LocalToWorldTransformFromEntity[turret.CannonBallSpawn];
var cannonBallTransform = UniformScaleTransform.FromPosition(spawnLocalToWorld.Value.Position);
cannonBallTransform.Scale = LocalToWorldTransformFromEntity[turret.CannonBallPrefab].Value.Scale;
ECB.SetComponent(instance, new LocalToWorldTransform
{
Value = cannonBallTransform
});
ECB.SetComponent(instance, new CannonBall
{
Speed = spawnLocalToWorld.Value.Forward() * 20.0f
});
+ // 아래의 줄은 포탑에서 대포알까지 색을 전파합니다.
+ ECB.SetComponent(instance, new URPMaterialPropertyBaseColor { Value = turret.Color });
}
}
"SafeZone Radius"는 아직 사용되지 않지만 이후 진행될 튜토리얼과 관련이 있습니다.
"Scripts/Systems" 폴더에 "TankSpawningSystem.cs"이라는 이름의 새 C# 스크립트 파일을 만들고 다음 내용을 저장합니다. ⚠ WARNING
이 시점에서 플레이 모드로 들어가면 탱크가 하나밖에 없다는 사실에 놀랄 수 있습니다. 실제로 20개의 탱크가 있지만, 그들은 모두 같은 위치에서 스폰되고 정확히 같은 방식으로 움직입니다.
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Rendering;
[BurstCompile]
partial struct TankSpawningSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
}
[BurstCompile]
public void OnDestroy(ref SystemState state)
{
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var config = SystemAPI.GetSingleton<Config>();
var ecbSingleton = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);
var vehicles = CollectionHelper.CreateNativeArray<Entity>(config.TankCount, Allocator.Temp);
ecb.Instantiate(config.TankPrefab, vehicles);
// 이 시스템은 시작할 때 한 번만 실행해야 합니다. 따라서 한 번 업데이트하면 자동으로 비활성화됩니다.
state.Enabled = false;
}
}
다음과 같이 "Scripts/Systems" 폴더에 있는 "TankMovementSystem.cs" 파일의 내용을 수정합니다:
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
partial class TankMovementSystem : SystemBase
{
protected override void OnUpdate()
{
var dt = SystemAPI.Time.DeltaTime;
Entities
.WithAll<Tank>()
- .ForEach((TransformAspect transform) =>
+ .ForEach((Entity entity, TransformAspect transform) =>
{
var pos = transform.Position;
+ // 이는 탱크의 실제 위치를 수정하는 것이 아니라 3D 노이즈 기능을 샘플링하는 지점만 수정합니다.
+ // 이 방식으로, 모든 탱크는 다른 파츠를 사용하고 있으며 고유한 랜덤 플로우 필드를 따라 이동합니다.
+ pos.y = entity.Index;
var angle = (0.5f + noise.cnoise(pos / 10f)) * 4.0f * math.PI;
var dir = float3.zero;
math.sincos(angle, out dir.x, out dir.z);
transform.Position += dir * dt * 5.0f;
transform.Rotation = quaternion.RotateY(angle);
}).ScheduleParallel();
}
}