Metaverse Picazoo
Overview
The PicaZoo scene is a mini game where the players have to find animal statues with a painting gun.
This demonstrates how to synchronize texture modification through the network.
If you want to develop a shooting game, please refer to the dedicated technical samples :
We use a specific controller in desktop mode to have a better gun control with the mouse.
PaintBlaster
The scene contains several painting guns :
- guns that shoot bullets of a predefined color
- guns that shoot bullets of a random color
Guns are managed by the PaintBlaster
class which is in charge to maintain two lists :
- list a shots
- list of impacts
Because lists are networked, remote players can check if a new shot/impact occurs and display them.
By doing this, there is no need to spawn a networked bullet prefab.
Shots
The properties of the shots are gathered in a BulletShoot
networked structure.
C#
public struct BulletShoot : INetworkStruct
{
public Vector3 initialPosition;
public Quaternion initialRotation;
public float shootTime;
public int bulletPrefabIndex;
public PlayerRef source;
}
First, user's input to shoot is handle by the InputActionProperty UseAction
which is used to set the IsUsed
bool.
C#
public bool IsUsed
{
get
{
return UseAction.action.ReadValue<float>() > minAction;
}
}
At each LateUpdate()
, we check if the local player grabs the gun and fires.
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();
...
}
For the desktop rig, the trigger is detected in DesktopAim
class.
C#
private void LateUpdate()
{
if (!grabber) return;
if (grabber.HasStateAuthority == false) return;
blaster.TriggerPressed(Mouse.current.leftButton.isPressed);
...
}
Managing shots consists of adding a new shot to the BulletShoots
networked list.
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);
...
}
Because the shots list is networked, all players receive updated data regardless of which player fired.
So, players just have to check in FixedUpdateNetwork()
if new data has been received in order to spawn a local graphical object when a new shot occurs (CheckShoots()
).
C#
const int MAX_BULLET_SHOOT = 50;
[Networked]
[Capacity(MAX_BULLET_SHOOT)]
public NetworkLinkedList<BulletShoot> BulletShoots { get; }
C#
public override void FixedUpdateNetwork()
{
base.FixedUpdateNetwork();
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;
}
}
}
Please note that the projectile game object spawned registers itself on the gun.
By doing this, we will be able to ensure that only the state authority of the gun will check if the projectile hits a target.
In order to limit the list size, the PurgeBulletShoots()
methods remove old shots from the list.
Indeed, it is useless to keep all shots indefinitely in the list because they were processed by all players as soon as they were added to the list and the propagation took place on the network.
In addition, the list of shots is cleared when the state authority changes, because the new authority probably does not have the same time reference as the previous one (IStateAuthorityChanged.StateAuthorityChanged()
).
Impacts
A networked structure ImpactInfo
has been created to save impact parameters.
C#
public struct ImpactInfo : INetworkStruct
{
public Vector2 uv;
public Color color;
public float sizeModifier;
public NetworkBehaviourId networkProjectionPainterId;
public float impactTime;
public PlayerRef source;
}
A new impact is added in RecentImpacts
networked list when the projectile collides with a target.
Then, the player having the gun state authority checks the list in FixedUpdateNetwork()
with CheckRecentImpacts()
method in order to request the object texture modification.
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;
}
}
}
Like the shots, in order to limit the impact list size, the PurgeRecentImpacts()
methods remove old impacts from the list.
Indeed, it is useless to keep all impacts indefinitely in the list because they were processed by all players as soon as they were added to the list and the propagation took place on the network.
PaintBlasterProjectile
During the FixedUpdate()
, the player with the gun state authority checks if the projectile will collide with an object.
If the projectile doesn't collide with an object, it will be despawned after a predefined timer.
C#
private void Update()
{
if (Time.time > endOfLifeTime)
{
Destroy(gameObject);
}
}
If the projectile collides with an object (determined by a raycast hit):
- the impact position is computed and added in the networked
RecentImpacts
list if the object texture should be modifiable (objects withProjectionPainter
&NetworkProjectionPainter
components) - the projectile is despawned
- an impact prefab with a particle system is spawned to generate a small visual effect
The network Impact
variable uv
field is filled with the RayCast
hit textureCoord
field.
To have a value in this textureCoord
field, some conditions are required:
- the object must have a mesh collider, with the same mesh than the one we want to paint
- in the FBX containing the mesh, its "Meshes > read/write" parameter has to be set to true
NetworkProjectionPainter & ProjectionPainter
The synchronization of texture modification over the network can be summarized with the following steps :
- the
NetworkProjectionPainter
received a texture modification request from the gun that fired the projectile withPaintAtUV()
method, - then it sends the request to the local
ProjectionPainter
component, which will actually perform the texture modification, - when the texture modification is terminated, the
NetworkProjectionPainter
is informed by a callback, - the player with the object state authority updates the network list which contains all the information on the impacts,
- remote players can update the texture of the object using their local
ProjectionPainter
component.
Now, let's see how it works in more detail.
step 1 : texture modification request
First, the NetworkProjectionPainter
received a texture modification request from the gun that fired the projectile with PaintAtUV()
method.
Only the player with state authority on the object is responsible to maintain the definitive texture state, but to avoid any delay when a remote player hits the object without having the state authority on it, a temporary texture modification is done by this remote player with PrePaintAtUV()
method.
C#
public void PaintAtUV(Vector2 uv, float sizeModifier, Color color)
{
if (Object.HasStateAuthority)
{
painter.PaintAtUV(uv, sizeModifier, color);
}
else
{
painter.PrePaintAtUV(uv, sizeModifier, color);
}
}
step 2 : texture modification
The local ProjectionPainter
is in charge to perform the object texture modification using the impact parameters received (UV coordinates, size & color of the projectile).
To explain the texture modification in few words :
- the texture of the object to paint is replaced by a temporary camera render texture,
- an impact brush in displayed in front of the texture at the correct uv coordinates at each hit,
- then it is captured by a camera which update the render texture so the player can see the new shooting impact,
- on a regular basis, the definitive object texture is updated by a new texture which includes all previous impacts. This operation is not performed on every hit to avoid resource consumption.
More explanation on the overall process can be found in this article Texture Painting
step 3 : texture modification callback
During the Awake()
, the ProjectionPainter
search for listeners (NetworkProjectionPainter
implements the IProjectionListener
interface).
C#
private void Awake()
{
listeners = new List<IProjectionListener>(GetComponentsInParent<IProjectionListener>());
}
So, when the texture modification is terminated, it can inform the 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);
}
}
step 4 : add the new impact in the list to inform remote players
The NetworkProjectionPainter
with state authority on the object can updates the network array which contains all the information on the impacts.
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 });
...
}
}
The impact parameters required to modify the texture are saved into a networked ProjectionInfo
structure.
All impacts are saved into a networked list ProjectionInfosLinkedList
.
So, all players are informed when a new impact is added to the list.
C#
public struct ProjectionInfo:INetworkStruct
{
public Vector2 uv;
public Color color;
public float sizeModifier;
public int paintId;
}
C#
[Networked(OnChanged = nameof(OnInfosChanged))]
[Capacity(MAX_PROJECTIONS)]
public NetworkLinkedList<ProjectionInfo> ProjectionInfosLinkedList { get; }
step 5 : texture updated by remote players
Now remote players can update the object texture by using the networked list ProjectionInfosLinkedList
and their local ProjectionPainter
component.
C#
static void OnInfosChanged(Changed<NetworkProjectionPainter> changed)
{
changed.Behaviour.OnInfosChanged();
}
void OnInfosChanged()
{
...
painter.PaintAtUV(info.uv, info.sizeModifier, info.color);
...
}
Back to top