- Home /
Understanding Unet attributes
Good day all. As a lot of newcomers I'm a bit confused with the way Unet attributes work. I was following one of youtube tutorial on making a multiplayer game, but then I decided to go on my own and implement grenades. So I've made some research both in the docs and youtube videos and came up with the following flow of this feature:
Player presses "G" button in attempt to throw a grenade.
Grenade prefab has NetworkIdentity and NetworkTransform components attached to it. The latter one has it's Network Send Rate set to 0. Generally speaking layout for grenade prefab is done like per this document on Unet Spawning
As player HAS authority to invoke commands, I spawn grenade on the server and grant client authority to it.
After some period of time grenade explodes, damages nearby objects and apply some force to them.
Generally speaking, I've almost got this simple flow working. The problem is that I'm a bit confused about grenade itself. What happens is that the client somehow invokes Damage function twice. I have one of my clients as a host and a client in the same time. Here is my code related to this feature.
PlayerShoot.cs:
[Client]
void ThrowExplosive()
{
Debug.Log("Attempt of grenade throw from " + GetComponentInParent<Player>().transform.name);
if ( isLocalPlayer == false)
{
return;
}
Debug.Log("Grenade thrown by " + GetComponentInParent<Player>().transform.name);
CmdSpawnGrenade();
}
[Command]
void CmdSpawnGrenade()
{
Debug.Log("Spawning grenade over the network by " + GetComponentInParent<Player>().transform.name);
GameObject grenade = Instantiate(currentThrowableItem.gameObject, cam.transform.position, cam.transform.rotation);
Rigidbody rb = grenade.GetComponent<Rigidbody>();
rb.velocity = cam.transform.forward * throwForce;
NetworkServer.SpawnWithClientAuthority(grenade, connectionToClient);
}
Grenade.cs
public class Grenade : NetworkBehaviour
{
public float delay = 3f;
public float explosionRadius = 7f;
public float maxDamage = 75f;
public float damageDropoff = 0f;
public float explosionForce = 700f;
public ParticleSystem explosionEffect;
bool hasExploded = false;
bool hasBeenThrown = false;
bool isDelayed = false;
float countdown;
void Start () {
countdown = delay;
}
// Update is called once per frame
void Update () {
countdown -= Time.deltaTime;
if (countdown <= 0f && !hasExploded)
{
hasExploded = true;
Debug.Log("Has exploded: " + hasExploded.ToString());
OnExplosion();
}
}
// Marking this with [ClientCallback] function works and prevents double call of
// particle play from grenades thrown by clients. Not sure if it's a correct way to do this
[ClientCallback]
void DisplayExplosionEffect()
{
// Display explosion effect
// Destroy it afterwards
Debug.Log("Call of RPC from Grenade. Should be called ONLY once!");
ParticleSystem _explosionEffect = Instantiate(explosionEffect, transform.position, Quaternion.identity);
StartCoroutine(Explode(_explosionEffect));
}
[Client]
void OnExplosion()
{
DisplayExplosionEffect();
ManageExplosive();
}
void ManageExplosive()
{
hasExploded = true;
DamageObjects();
ApplyForceToObjects();
}
IEnumerator Explode(ParticleSystem _explosionEffect)
{
gameObject.GetComponent<MeshRenderer>().enabled = false;
_explosionEffect.Play();
yield return new WaitUntil(() => _explosionEffect.isPlaying == false);
// Destroy explosion effect
Destroy(_explosionEffect.gameObject);
// Destroy it's parent
//Destroy(gameObject);
}
// The most intriguing part. As I understand, this SHOULD be a [Command], but in this case
// this method is being called twice if grenade is spawned by the client.
// If I have it like this, this brings is being executed only once, BUT I get a warning that I try
// to call ClientRpc from the Client instead of server. I guess it means that this object
// (grenade, I suppose?) is both client and server, and somehow calls this function twice,
// as a client and a server. How is that possible?
[Client]
private void DamageObjects()
{
Debug.Log("DamageObjects called");
Collider[] objectsToDamage = Physics.OverlapSphere(gameObject.transform.position, explosionRadius);
foreach (Collider objectToDamage in objectsToDamage)
{
Debug.Log("Object name is: " + objectToDamage.name);
Player player = objectToDamage.GetComponentInParent<Player>();
if (player != null)
player.RpcTakeDamage(maxDamage);
}
}
[Client]
private void ApplyForceToObjects()
{
Debug.Log("ApllyForceToObjects called");
Collider[] objectsToDamage = Physics.OverlapSphere(gameObject.transform.position, explosionRadius);
foreach (Collider objectToPush in objectsToDamage)
{
Rigidbody rb = objectToPush.GetComponent<Rigidbody>();
if (rb != null)
rb.AddExplosionForce(explosionForce, gameObject.transform.position,
explosionRadius, 0.5f);
}
}
}
RpcTakeDamage from Player.cs
[ClientRpc]
public void RpcTakeDamage(float _damage)
{
if (isDead)
return;
currentHealth -= _damage;
if (currentHealth <= 0)
{
Die();
}
Debug.Log(transform.name + " took damage.");
Debug.Log(transform.name + " now has " + currentHealth + " health");
}
So here it is. I'm really confused with the call stack and why would grenade script call DamageObjects() twice. I feel like I've messed up with something I don't quite understand, so I'll appreciate any help. Thank you in advance!
Answer by Bunny83 · Jan 24, 2018 at 04:50 AM
Uhh. Where should i start here ^^.
First of all in many places you have completely reversed the logic of an authoritative server. Any kind of logic should run exclusively on the server. That means all the damage dealing is only done on the server. When you use sync vars for things like health the change will automatically be distributed to the clients. You probably want to have a client callback to execute / show some visuals (like instantiating / fireing the explosion).
Next thing is the player doesn't need any authority over the grenade object itself. It's just a world object that is controlled entirely by the server.
Any kind of physics object which could be affected by the explosion should be a networked object as well. So when you move it on the server the movement would automatically be synced with all clients.
So the usual event flow is more like this:
On the client (only for the local client) we check if the "G" button has been pressed. We might do a check if we actually have grenades left but this is optional. The actual check should be done on the server.
Now we actually call a command method to throw a grenade. This will be executed on the server for our player object.
If conditions are met (we have a grenade, maybe some cooldown, ...) we instantiate a grenade object and throw it. For counting scores / kills you may want to store the owner inside the grenade script, but that's up to you.
At this point the player object which send the command has nothing to do with the rest. The instantiated grenade will do it's "thing". We check the countdown on the server and when it should explode we do all the required things on the server. This includes the subtraction of the health of the affected players / objects as well as pushing them. If you want / need to seperate certain sub functions you may want to use the "Server" attribute to ensure only the server can call these.
I strongly recommend that you read the following pages really carefully: UNet Player Objects, Object Spawning, State Synchronization and Scene Objects. Also this general concept of Commands and ClientRPCs might be helpful
Thank you so much for your answer. Had to go to the work right now, but sure I'll try your suggestion later on. Also, many thanks for providing links to the docs! If I get your point right, the only thing player does is just throws a grenade by sending a command? All other things are handled by server ONLY and translated back to the client?
Thank you for your suggestion, I've got a working prototype. Will share it this weekend.
Answer by Amrait · Feb 04, 2018 at 10:00 AM
@Bunny83 , sorry for delay getting back - actually, the first prototype was both incorrect (in it's core, although worked well) and poorly optimized. Can't claim that this one is a top notch, but it is definitely better. In case if someone is wondering, here is what I've got:
Grenade.cs
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;
public class Grenade : NetworkBehaviour
{
[SerializeField] private float delay = 3f;
[SerializeField] private float explosionRadius = 7f;
[SerializeField] private float maxDamage = 75f;
[SerializeField] private float damageDropoff = 0f;
[SerializeField] private float explosionForce = 700f;
[SerializeField] private LayerMask mask;
public ParticleSystem explosionEffect;
bool hasExploded = false;
bool hasBeenThrown = false;
bool isDelayed = false;
float countdown;
void Start () {
countdown = delay;
}
// Update is called once per frame
void Update () {
// Check if countdown reached zero
countdown -= Time.deltaTime;
// if it's lower or equal to zero and grenade hasn't exploded yet
if (countdown <= 0f && !hasExploded)
{
// Setting some logs
Debug.Log("Has exploded: " + hasExploded.ToString());
// Perform OnExplosion() method, which will take care of everything else
OnExplosion();
}
}
/// <summary>
/// Will take care of displaying effects and managing every other logic.
/// Called on the server
/// </summary>
void OnExplosion()
{
DisplayExplosionEffect();
ManageExplosive();
}
/// <summary>
/// Method to handle logic outside of the coroutine
/// </summary>
void ManageExplosive()
{
// Mark this grenade as exploded
hasExploded = true;
// Get position of the explosion center
Vector3 explosionPosition = gameObject.transform.position;
// Apply damage to nearby objects
CmdDamageObjects(explosionPosition);
// Apply forces
// RpcApplyForceToObjects();
}
/// <summary>
/// Coroutine to display explosion effects
/// </summary>
/// <param name="_explosionEffect">Exact instance of explosion effect to display</param>
/// <returns>IEnumerator to wait for the end of the execution</returns>
IEnumerator Explode(ParticleSystem _explosionEffect)
{
// Disable mesh renderer on the grenade so it visually destroyed
gameObject.GetComponent<MeshRenderer>().enabled = false;
// Play the explosion effect.
_explosionEffect.Play();
// Stop the execution of this coroutine until particles ended
yield return new WaitUntil(() => _explosionEffect.isPlaying == false);
// Destroy explosion effects game object i.e. particle
Destroy(_explosionEffect.gameObject);
// Unspawn grenade on the server
CmdUnSpawnGrenade();
}
/// <summary>
/// Command to unspawn grenade on the server. Will be executed only on the server.
/// </summary>
[Command]
private void CmdUnSpawnGrenade()
{
// Unspawn grenade on the server so it does not exist anymore
NetworkServer.UnSpawn(gameObject);
// Destroy game object at all
Destroy(gameObject);
}
// ---------> Works partially, need better implementation of cover system
/// <summary>
/// Command to damage objects near the grenade. Will be executed only on the server.
/// </summary>
[Command]
private void CmdDamageObjects(Vector3 explosionPosition)
{
// Collect all of the colliders around
// TODO: add a layer mask so it wont grab just colliders,
// only damageable objects
Collider[] objectsToDamage = Physics.OverlapSphere(gameObject.transform.position, explosionRadius);
float _exposedModifier = 0f;
for (int i = 0; i < objectsToDamage.Length; i++)
{
Player player = objectsToDamage[i].GetComponent<Player>();
if (player != null)
{
// Calculate how much target is exposed
_exposedModifier = CalculateExposedModifier(explosionPosition,
player.gameObject.GetComponent<Collider>());
// Calculate distance between explosion point and the target
float distance = (player.transform.position - explosionPosition).magnitude;
// Apply damage
player.RpcTakeDamage(CalculateDamageOverRange(distance, damageDropoff, maxDamage, _exposedModifier));
// Apply force
RpcApplyForceToObjects(player.gameObject);
}
}
}
/// <summary>
/// Client callback to apply forces to objects. Will be executed on all of the clients
/// individually.
/// </summary>
[ClientRpc]
private void RpcApplyForceToObjects(GameObject gameObject)
{
Rigidbody rb = gameObject.GetComponent<Rigidbody>();
rb.AddExplosionForce(explosionForce, gameObject.transform.position,
explosionRadius, 0.5f);
}
/// <summary>
/// Client callback to display explosion effects for every client. Will be executed on all of the clients
/// individually.
/// </summary>
[ClientCallback]
void DisplayExplosionEffect()
{
// Display explosion effect
// Destroy it afterwards
// Instantiate explosion effect at position of the grenade
ParticleSystem _explosionEffect = Instantiate(explosionEffect, transform.position, Quaternion.identity);
// Start coroutine to display explosion effect.
// Coroutine will wait until explosion effect will be fully played.
// After that, Explode() function will call cleanups after that.
StartCoroutine(Explode(_explosionEffect));
}
private float CalculateDamageOverRange(float distance, float damageDropoff, float maxDamage, float exposedModifier)
{
// TODO:
// think of a proper algorith. Return maxDamage for now
return maxDamage * exposedModifier;
}
private float CalculateExposedModifier(Vector3 explosionPoint, Collider damagedTarget)
{
float exposedModifier = 1f;
float extentX = damagedTarget.bounds.extents.x;
float extentY = damagedTarget.bounds.extents.y;
float extentZ = damagedTarget.bounds.extents.z;
RaycastHit [] _hits;
Vector3 center = damagedTarget.bounds.center;
// Generate list of points manually
Vector3 [] points = new Vector3[]
{
center,
new Vector3(center.x, center.y + extentY, center.z), // full head
new Vector3(center.x, center.y, center.z + extentZ), // full right
new Vector3(center.x, center.y, center.z - extentZ), // full left
new Vector3(center.x, center.y + (extentY/2), center.z + (extentZ/2)), // midway right-up
new Vector3(center.x, center.y + (extentY/2), center.z - (extentZ/2)), // midway left-up
new Vector3(center.x, center.y - extentY, center.z) // bottom
};
for (int i = 0; i < points.Length; i++)
{
_hits = Physics.RaycastAll(explosionPoint, (points[i] - explosionPoint), (points[i] - explosionPoint).magnitude);
for (int j = 0; j < _hits.Length; j++)
{
if (_hits[j].collider.GetComponent<Player>() == null)
{
Debug.Log("No player component");
exposedModifier = exposedModifier - 0.143f;
break;
}
}
}
Debug.Log("Exposed modifier is: " + exposedModifier.ToString());
return exposedModifier;
}
}
Grenade is thrown by this method from PlayerShoot.cs:
void ThrowExplosive()
{
if ( isLocalPlayer == false)
{
return;
}
CmdSpawnGrenade();
}
[Command]
void CmdSpawnGrenade()
{
GameObject grenade = Instantiate(currentThrowableItem.gameObject, cam.transform.position, cam.transform.rotation);
Rigidbody rb = grenade.GetComponent<Rigidbody>();
rb.velocity = cam.transform.forward * throwForce;
NetworkServer.Spawn(grenade);
}
As you can see, this solution isn't generic at all, for instance, if I want to do some smoke bombs or gas grenades I would like to have some sort of a base class for this. Also, I believe that this can be optimized a lot better, so this is my aim for the future, especially object pooling. In any case, thank you for your help