Collectibles
In this asset, collectibles are networked, uncontrolled scene objects with a component deriving from Collectible
attached to them. The owner of collectibles is the server, which is also responsible for synchronizing their current state to newly joined or already connected players in a match. Because of this, each Collectible
item has a managing ObjectSpawner
as a parent in the scene. Also they feature two different types, one that consumes them immediately and one that allows player to carry the item around.
Spawner
The ObjectSpawner
component is responsible for the actual spawning instruction of a Collectible
in the scene, that lets you define the prefab, exact position and respawn delay. Since it will spawn the Collectible
as a networked scene object, the ObjectSpawner
game object also needs to have a NetworkObject / PhotonView
component attached. After the ObjectSpawner
spawned a object, it assigns a reference to itself on the Collectible
, so we can later recognize at any point to which ObjectSpawner
an item belongs to. This is very important, since the Collectible
will inform the ObjectSpawner
of various events involving state changes, i.e. whether it was just consumed or picked up and should be respawned soon!
- Netcode
- Photon
The reference from a Collectible
to its parent ObjectSpawner
is saved using its NetworkObjectId in a NetworkVariable
directly on the Collectible
. We do it that way because when networked objects are spawned for other clients, they exist without clear allocation. This means we cannot just send the ObjectSpawner
reference over because this reference does not exist on target clients - they would have their own reference, not matching with the server's. By using and syncing the NetworkObjectId across all clients, which is a globally unique ID and actually the same on all clients, we can instead go from the Collectible
's NetworkObject
component and get its corresponding ObjectSpawner
parent.
Using Photon, the Collectible
does not needs its own PhotonView
since each instance can be handled locally on the client. Therefore the relationship between collectibles and their spawner is set locally too, directly via the ObjectSpawner
that instantiates the item.
Prefabs for all types of collectibles are located in the project under TanksMultiplayer > Prefabs
. Just like bullets, collectibles have their own Pool
in the game scene making use of our PoolManager
in their spawn behavior.
Use Type
A Collectible
item for consumption is typically an object that applies to a player in order to boost their effectivity in the game, like powerups. For this type, the Collectible
script is working like a template for different powerups, such as for the PowerupBullet
or PowerupHealth
scripts. The item can be in one out of two possible states as listed in the table below.
State | Description |
---|---|
Active | Visible in the scene and can be collected by colliding with any player. Calls Apply() on the colliding player to apply the effect. |
Inactive | Currently invisible in the scene because it has been consumed recently. The server is running a respawn timer to reactivate it soon. |
If you would like to create new powerups, simply extend your script from Collectible
and override its Apply() method. This method is responsible for what to do when a player collects the powerup, so all of your consumption logic should go in here.
Pickup Type
A Collectible
pickup item describes an object that resides at a default position, but can picked up and then carried around by a player. As a use-case, you could think of a "Rambo" item that either makes a player invisible or overpowered as long as it is attached to that player. In the Capture The Flag mode, this type of Collectible
is used for the flags. However an extension was made on the class to let it be assigned to a team, namely the CollectibleTeam
component, adding the Team Index variable. The item can be in one of several different states as listed in the table below.
State | Description |
---|---|
Default | Active at the default position and can be collected by colliding with any player. Calls Apply() to check the condition whether the object can be picked up by this player. Then calls Pickup() to attach the object to that player. |
Carried | Currently attached to a player. In the event that player is killed, Drop() is called, making the object available on the dropped position for other players to pick up. The item can also be taken to a destination zone (more on this below), e.g. adding points to a team and then calling Return() to reset the state to Default . |
Dropped | The item was dropped by a player and is currently not located at its default position. It can be picked up by other players, otherwise Return() is called by the server after a delay, in order to reset it to its Default state. |
For the sake of covering several different network approaches in this asset, a new type of synchronization is used for the pickup/drop/return of a CollectibleTeam
.
- Netcode
- Photon
For synchronizing important variables of a CollectibleTeam
, we could use multiple single NetworkVariables
or NetworkLists
. But, we can also define a class or struct with custom values and synchronize that. The CollectibleState
struct, located in the ObjectSpawner
script at the bottom, is used to define a Collectible
's network ID, parent ID and position. In order to synchronize it across the network, the struct needs to derive from INetworkSerializable
. To allow synchronization of multiple struct entries, we therefore use a NetworkList< CollectibleState >. The implementation is defined in the GameManager
, with a callback subscription for changed entries invoking OnCollectibleStateChanged().
We have already used RPCs in another section - but the thing is, RPCs are only executed at the time they are received from other clients. With a CollectibleTeam
, we would want to use a "persistent" RPC that can be accessed by newly joined players, without letting the server send all previous item states in multiple RPCs manually. For this use-case, buffered RPCs come to a rescue. Buffered RPCs are like regular RPCs, but remembered on the server and executed for newly joining players automatically. For this, the CollectibleTeam
component sends a regular RPC to other clients on pickup and drop events via its ObjectSpawner
, passing in RpcTarget.AllBuffered on the call.
Depending on the action executed (pickup or drop), an additional variable (carrier or position) is then provided to all clients along with it. An exception is the RPC to return a dropped Collectible
, which is not buffered: having the Collectible
at its original position is the default, so we do not have to explicitly save that for connecting players - the Collectible
will spawn there anyway.
With our implementation for CollectibleTeam
change events, we would not want to store an infinite amount of changed entries every time a player picks up or drops the Collectible
though – just the most recent action. For this, we are searching for an existing entry about it first and then modify that or remove all others. Note that the implementation as a whole is basically just a sample for demonstrating a different networked approach. Since we only store the most recent change, the same functionality could also be achieved easily by storing the necessary variables on the Collectible
itself, or letting the clients request them from the server via a single RPC call.
Collectible Zone
With the networked functionality for pickups above, we still need a way to detect if a player has scored by getting the enemy flag to its own base. In the Capture The Flag mode, we have added the CollectibleZone
component for this reason.
The CollectibleZone
component should include a BoxCollider
for detecting player or Collectible
collisions. It is attached to a game object in the scene, defining the "Home Base" for one team. If an ObjectSpawner
is assigned to the Require Object variable, that specific object needs to be present at its home base before the player can score. In the Capture The Flag mode, this means that e.g. Team = 0 (Red Team) can only score by returning the blue flag, if the required object (their own - red flag) is at their base. With the Require Object unassigned, players could also score when someone else has taken their own flag.