This document is about: FUSION 2
SWITCH TO

Metaverse Picazoo


Available in the Industries Circle
Circle

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.

Fusion Metaverse PicaZoo

PaintBlaster

The scene contains several painting guns :

  • guns that shoot bullets of a predefined color
  • guns that shoot bullets of a random color
Fusion Metaverse Guns

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 Render() 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 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;
        }
    }
}

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 Render() 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 with ProjectionPainter & 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

Fusion Metaverse Animal

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 with PaintAtUV() 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]
[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#

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