메타버스 Picazoo
개요
PicaZoo 씬은 플레이어가 페인팅 건을 들고 동물 동상을 찾아야 하는 미니 게임입니다.
이는 네트워크를 통해 텍스처 수정을 동기화하는 방법을 보여줍니다.
슈팅 게임을 개발하려면 전용 기술 샘플을 참조하십시오:
데스크톱 모드에서 특정 컨트롤러를 사용하여 마우스로 더 나은 총기 제어 기능을 제공합니다.
PaintBlaster
이 씬은 여러 개의 페인팅 건을 포함하고 있습니다:
- 미리 정해진 색을 발사하는 페인팅 건
- 무작위의 색을 발사하는 페인팅 건
페인팅 건은 PaintBlaster
클래스에 의해 관리되며, 이 클래스는 두 개의 목록을 유지합니다:
- 발사 목록
- 영향 목록
목록은 네트워크로 연결되어 있기 때문에 원격 플레이어는 새로운 샷/임팩트가 발생하는지 확인하고 표시할 수 있습니다.
이렇게 하면 네트워크로 연결된 총알 프리팹을 생성할 필요가 없습니다.
발사
샷의 속성은 BulletShoot
네트워크 구조체로 수집됩니다.
C#
public struct BulletShoot : INetworkStruct
{
public Vector3 initialPosition;
public Quaternion initialRotation;
public float shootTime;
public int bulletPrefabIndex;
public PlayerRef source;
}
첫째, 사용자의 촬영 입력은 IsUsed
부울 설정에 사용되는 InputActionProperty UseAction
에 의해 처리됩니다.
C#
public bool IsUsed
{
get
{
return UseAction.action.ReadValue<float>() > minAction;
}
}
각 LateUpdate()
에서 로컬 플레이어가 총을 잡고 발사하는지 확인합니다.
C#
public void LateUpdate()
{
...
if (Object == null || Object.HasStateAuthority == false) return;
if (useInput) // VR
{
if (IsGrabbedByLocalPLayer)
{
TriggerPressed(IsUsed);
}
else
wasUsed = false;
}
...
PurgeBulletShoots();
PurgeRecentImpacts();
}
C#
public void TriggerPressed(bool isPressed)
{
...
if (isPressed)
Shoot();
...
}
데스크톱 리그의 경우 DesktopAim
클래스에서 트리거가 감지됩니다.
C#
private void LateUpdate()
{
if (!grabber) return;
if (grabber.HasStateAuthority == false) return;
blaster.TriggerPressed(Mouse.current.leftButton.isPressed);
...
}
발사 관리는 BulletShoots
네트워크 목록에 새로운 샷을 추가하는 것으로 구성됩니다.
C#
public void Shoot()
{
OnShooting(projectileOriginPoint.transform.position, projectileOriginPoint.transform.rotation);
}
C#
private void OnShooting(Vector3 position, Quaternion rotation)
{
...
lastBullet.initialPosition = position;
lastBullet.initialRotation = rotation;
lastBullet.shootTime = Time.time;
lastBullet.bulletPrefabIndex = bulletPrefabIndex;
lastBullet.source = Object.StateAuthority;
BulletShoots.Add(lastBullet);
...
}
샷 목록이 네트워크로 연결되어 있기 때문에 어떤 플레이어가 발사되었는지에 관계없이 모든 플레이어가 업데이트된 데이터를 받습니다.
따라서 플레이어는 새로운 샷이 발생했을 때 로컬 그래픽 객체를 생성하기 위해 새로운 데이터가 수신되었는지 Render()
를 확인하기만 하면 됩니다(CheckShoots()
).
C#
const int MAX_BULLET_SHOOT = 50;
[Networked]
[Capacity(MAX_BULLET_SHOOT)]
public NetworkLinkedList<BulletShoot> BulletShoots { get; }
C#
public override void Render()
{
base.Render();
CheckShoots();
CheckRecentImpacts();
}
C#
void CheckShoots()
{
foreach (var bullet in BulletShoots)
{
if (bullet.shootTime > lastSpawnedBulletTime && bullet.source == Object.StateAuthority)
{
var bulletPrefab = (bullet.bulletPrefabIndex == -1) ? defaultProjectilePrefab : projectilePrefabs[bullet.bulletPrefabIndex];
var projectileGO = GameObject.Instantiate(bulletPrefab, bullet.initialPosition, bullet.initialRotation);
var projectile = projectileGO.GetComponent<PaintBlasterProjectile>();
projectile.sourceBlaster = this;
lastSpawnedBulletTime = bullet.shootTime;
}
}
}
발사체 게임 객체가 생성되어 페인팅 건에 등록됩니다.
이렇게 함으로써 발사체가 목표물에 부딪히는지 여부를 페인팅 건의 주 상태 권한만이 확인할 수 있도록 할 수 있을 것입니다.
목록 크기를 제한하기 위해 PurgeBulletShoots()
메소드는 목록에서 오래된 샷을 제거합니다.
실제로 모든 샷이 리스트에 추가되자마자 모든 플레이어가 처리했고 네트워크에서 전파가 이뤄졌기 때문에 무제한으로 리스트에 유지하는 것은 소용이 없습니다.
또한 상태 권한이 바뀌면 샷 리스트가 지워지는데, 이는 상태 권한이 이전과 동일한 시간 기준을 가지고 있지 않을 가능성이 있기 때문입니다(IStateAuthorityChanged.StateAuthorityChanged()
).
영향
영향 파라미터를 절약하기 위한 네트워크 구조체 ImpactInfo
가 생성되었습니다.
C#
public struct ImpactInfo : INetworkStruct
{
public Vector2 uv;
public Color color;
public float sizeModifier;
public NetworkBehaviourId networkProjectionPainterId;
public float impactTime;
public PlayerRef source;
}
발사체가 목표물과 충돌하면 RecentImpacts
네트워크 목록에 새로운 영향이 추가됩니다.
그런 다음 총기 국가 권한을 가진 플레이어는 Render()
에서 CheckRecentImpacts()
메소드로 목록을 확인하여 객체 텍스처 수정을 요청합니다.
C#
const int MAX_IMPACTS = 50;
[Networked]
Capacity(MAX_IMPACTS)]
public NetworkLinkedList<ImpactInfo> RecentImpacts { get; }
C#
void CheckRecentImpacts()
{
foreach (var impact in RecentImpacts)
{
if (impact.impactTime > lastRecentImpactTime && impact.source == Object.StateAuthority)
{
if (Runner.TryFindBehaviour<NetworkProjectionPainter>(impact.networkProjectionPainterId, out var networkProjectionPainter))
{
if (networkProjectionPainter.HasStateAuthority || Object.HasStateAuthority)
{
networkProjectionPainter.PaintAtUV(impact.uv, impact.sizeModifier, impact.color);
}
}
lastRecentImpactTime = impact.impactTime;
}
}
}
발사와 마찬가지로 충격 목록 크기를 제한하기 위해 PurgeRecentImpacts()
메소드는 목록에서 오래된 충격을 제거합니다.
실제로 모든 영향이 목록에 추가되자마자 모든 플레이어에 의해 처리되고 네트워크에서 전파가 이루어졌기 때문에 모든 영향을 무한정 목록에 유지하는 것은 소용이 없습니다.
PaintBlasterProjectile
FixedUpdate()
에서 페인팅 건 상태 권한을 가진 플레이어는 발사체가 물체와 충돌할지 여부를 확인합니다.
발사체가 물체와 충돌하지 않으면 미리 정의된 타이머 후에 발사체가 소멸됩니다.
C#
private void Update()
{
if (Time.time > endOfLifeTime)
{
Destroy(gameObject);
}
}
발사체가 물체와 충돌하는 경우(레이 캐스트 히트에 의해 결정됨):
- 객체 텍스처를 수정할 수 있어야 하는 경우 네트워크로 연결된
RecentImpacts
목록에 충격 위치를 계산하여 추가합니다(ProjectionPainter
및NetworkProjectionPainter
컴포넌트가 있는 객체) - 발사체가 소멸되었습니다
- 작은 시각적 효과를 발생시키기 위해 파티클 시스템이 있는 임팩트 프리팹이 생성됩니다
네트워크 Impact
변수 uv
필드는 RayCast
히트 textureCoord
필드로 채워집니다.
이 textureCoord
필드에서 값을 얻으려면 몇 가지 조건이 필요합니다:
- 물체에는 우리가 칠하려는 것과 동일한 메쉬가 있는 메시 콜라이더가 있어야 합니다
- 메시가 포함된 FBX에서 "Meshes > read/write" 매개 변수를 true로 설정해야 합니다
NetworkProjectionPainter & ProjectionPainter
네트워크를 통한 텍스처 수정의 동기화는 다음 단계로 요약할 수 있습니다:
NetworkProjectionPainter
는PaintAtUV()
메소드로 발사체를 발사한 총기로부터 질감 수정 요청을 받았고- 그런 다음 실제로 텍스처 수정을 수행할 로컬
ProjectionPainter
컴포넌트로 요청을 전송합니다, - 텍스처 수정이 종료되면 콜백으로
NetworkProjectionPainter
에게 알려주고, - 객체 상태 권한을 가진 플레이어는 영향에 대한 모든 정보를 포함하는 네트워크 목록을 업데이트합니다,
- 원격 플레이어는 로컬
ProjectionPainter
컴포넌트를 사용하여 객체의 텍스처를 업데이트할 수 있습니다.
이제 어떻게 작동하는지 좀 더 자세히 살펴보겠습니다.
단계 1 : 텍스처 변경 요청
먼저, NetworkProjectionPainter
는 PaintAtUV()
메소드로 발사체를 발사한 총으로부터 텍스쳐 수정 요청을 받았습니다.
객체에 대한 상태 권한을 가진 플레이어만이 최종 텍스처 상태를 유지해야 하지만, 원격 플레이어가 상태 권한을 가지지 않은 상태에서 객체에 부딪혔을 때 지연되는 것을 방지하기 위해 이 원격 플레이어는 PrePaintAtUV()
메소드로 임시 텍스처 수정을 수행합니다.
C#
public void PaintAtUV(Vector2 uv, float sizeModifier, Color color)
{
if (Object.HasStateAuthority)
{
painter.PaintAtUV(uv, sizeModifier, color);
}
else
{
painter.PrePaintAtUV(uv, sizeModifier, color);
}
}
단계 2 : 텍스처 변경
수신된 충격 파라미터(UV 좌표, 발사체의 크기 및 색상)를 사용하여 물체 텍스처 수정을 수행하는 것은 로컬 ProjectionPainter
에서 담당합니다.
텍스처 수정을 몇 단어로 설명하려면:
- 페인트칠 대상물의 텍스처는 임시 카메라 렌더 질감으로 대체됩니다,
- 각 히트 시 정확한 UV 좌표로 텍스처 앞에 표시되는 임팩트 브러시,
- 그런 다음 렌더 텍스처를 업데이트하는 카메라에 의해 캡처되어 플레이어가 새로운 촬영 효과를 볼 수 있습니다,
- 정기적으로 모든 이전 영향을 포함하는 새로운 텍스처에 의해 최종 객체 텍스처가 업데이트됩니다. 이 작업은 리소스 소비를 피하기 위해 모든 히트에 대해 수행되지 않습니다.
전체 공정에 대한 자세한 설명은 이 아티클에서 확인할 수 있습니다 텍스처 페인팅.
단계 3 : 텍스처 변경 콜백
Awake()
동안 ProjectionPainter
는 리스너를 위한 검색(NetworkProjectionPainter
는 IProjectionListener
인터페이스를 구현합니다)을 합니다.
C#
private void Awake()
{
listeners = new List<IProjectionListener>(GetComponentsInParent<IProjectionListener>());
}
따라서 텍스처 수정이 종료되면 NetworkProjectionPainter
에 알릴 수 있습니다.
C#
public void PaintAtUV(Vector2 uv, float sizeModifier, Color color)
{
var paintPosition = UV2CaptureLocalPosition(uv);
Paint(paintPosition, sizeModifier, color);
foreach (var listener in listeners)
{
listener.OnPaintAtUV(uv, sizeModifier, color);
}
}
단계 4 : 원격 플레이어에게 알리기 위해 목록에 새로운 영향 추가
개체에 대한 상태 권한을 가진 NetworkProjectionPainter
는 영향에 대한 모든 정보를 포함하는 네트워크 배열을 업데이트할 수 있습니다.
C#
public void OnPaintAtUV(Vector2 uv, float sizeModifier, Color color)
{
if (Object.HasStateAuthority)
{
...
ProjectionInfosLinkedList.Add(new ProjectionInfo { uv = uv, sizeModifier = sizeModifier, color = color, paintId = nextStoredPaintId });
...
}
}
텍스처를 수정하는 데 필요한 영향 파라미터는 네트워크로 연결된 ProjectionInfo
구조체에 저장됩니다.
모든 영향은 네트워크 목록인 ProjectionInfosLinkedList
에 저장됩니다.
따라서 새로운 임팩트가 목록에 추가되면 모든 플레이어에게 알려줍니다.
C#
public struct ProjectionInfo:INetworkStruct
{
public Vector2 uv;
public Color color;
public float sizeModifier;
public int paintId;
}
C#
[Networked]
[Capacity(MAX_PROJECTIONS)]
public NetworkLinkedList<ProjectionInfo> ProjectionInfosLinkedList { get; }
단계 5 : 원격 플레이어에 의한 텍스처 업데이트
이제 원격 플레이어는 네트워크 목록 ProjectionInfosLinkedList
와 로컬 ProjectionPainter
컴포넌트를 사용하여 객체 텍스처를 업데이트할 수 있습니다.
C#
ChangeDetector changeDetector;
public override void Render()
{
base.Render();
foreach (var changedVar in changeDetector.DetectChanges(this))
{
if (changedVar == nameof(ProjectionInfosLinkedList))
{
OnInfosChanged();
}
}
}
void OnInfosChanged()
{
...
painter.PaintAtUV(info.uv, info.sizeModifier, info.color);
...
}
Back to top