- Home /
Dealing damage over a network
Hey guys, I am currently trying to understand more about multiplayer. I have currently read the documentation on this issue but still can't get it working and would like to know where I'm going wrong.
The Goal: Each player in the game is able to shoot (using raycasting) and can apply damage over a network to the correct plane that has been shot.
Current Implementation: Currently I have a Plane.cs script that controls each plane (player) on the network.
Plane.cs (I've commentated how I'm expecting it should work):
// This value will be updated on clients as soon as the server updates it.
[SyncVar]
int health = 100;
// A local variable for each client to determine if they can shoot their gun
bool canShoot = true;
// In the fixed update method, only process if we have authority over the plane
void FixedUpdate () {
if (!hasAuthority) {
return;
}
if(Input.GetKey(KeyCode.Space)){
if (canShoot) {
// Only if the the player hasAuthority AND they canShoot do we call the Shoot Co-routine
canShoot = false;
StartCoroutine(Shoot ());
}
}
}
IEnumerator Shoot () {
CmdShootBullet (shootLeftGun, gameObject.GetComponent<NetworkIdentity>());
yield return new WaitForSeconds(0.5f);
canShoot = true;
}
// This command will run on the server
[Command]
void CmdShootBullet(bool shootLeft , NetworkIdentity plane){
//The server first determines what position it needs to shoot from
Transform gunSpawner;
if (shootLeft) {
gunSpawner = plane.transform.Find ("PlaneUnit/leftGunSpawner");
} else {
gunSpawner = plane.transform.Find ("PlaneUnit/rightGunSpawner");
}
// The bullet is then spawned by the server and a force is applied (NOTE: this is only for visual
effects)
GameObject spawnedBullet = Instantiate (bullet, gunSpawner.position, gunSpawner.rotation);
NetworkServer.Spawn (spawnedBullet);
spawnedBullet.GetComponent<Rigidbody> ().AddRelativeForce (Vector3.forward * 10000,ForceMode.Impulse);
// As the server check if raycasting hits a plane
RaycastHit hit;
if (Physics.Raycast (gunSpawner.position, Vector3.forward, out hit)) {
if (hit.transform.tag == "Plane") {
// If it does hit a plane, then generate a random amount of damage and apply the damage to the plane that has been hit
int damageTaken = Random.Range (5, 15);
hit.transform.gameObject.GetComponent<SpitfireControls>().CmdDamage(damageTaken);
}
}
}
// This command also gets run on the server, on the plane's script that got hit
[Command]
void CmdDamage(int damage)
{
// The health is decreased on the server for the plane script that got hit, then should automatically update on clients because the SyncVar value has changed
health -= damage;
}
Currently if the Host shoots the Client OR the Client shoots the Host, the Host's health is always reduced but never the Clients. Why is this? What am I misunderstanding?
Any help on this would be very much appreciated! Thanks in advance :)
Hey @Bunny83 , I wondered if I could have some help with this, whenever I see you answer a question it's usually always well explained! If you could shed any light on my problem it would be greatly appreciated :)
P.S Sorry if I'm not supposed to tag you directly.
Answer by Bunny83 · Jan 08, 2019 at 10:42 AM
Ok there are a few things you want to change.
First of all keep in mind that a "Command" method is always meant to be called from a client and executed on the server. Currently you execute CmdDamage from inside another command (CmdShootBullet) which makes no sense. The method "CmdShootBullet" is already executed on the server and the server just want to subtract the damage from this gameobject. So you may want to turn the CmdDamage into a normal method and just put the [Server]
attribute on it. You don't want clients to be able to call this method. Even they can only hurt themselfs which seems pretty pointless, they can however call this method with a negative number to gain health ^^.
The next thing is the NetworkIdentity plane you send to your "CmdShootBullet" method is pretty pointless. Commands are executed on the gameobjects they belong to. So if a player executes this method on their own plane object, the method will be called on the server on the corresponding plane object in the server scene. Your "gunSpawner" variable you want to setup as a public variable / array and just assign the corresponding spawner objects in your prefab. Using transform.Find every time is unnecessary. Instead of the shootLeft bool you could simply send an index into the spawner / weapon array. This would be future proof if you decide there are more than two weapons the player can use.
Those were the major issues i see in your script. There are a few other things you may want to change for performance:
Starting a new coroutine each time the player should fire (2 times a second) is not a good idea. Coroutines are objects which are created when you start one. It would be better to have a single coroutine which does the input check in a infinite loop and just wait in place once a shot has been fired.
Don't use FixedUpdate for input checking. FixedUpdate is part of the physics system and represents a single physics step. It doesn't run in sync with the visual framerate. If you use a single looping coroutine this would be solved as well.
Be aware that your bullet can actually miss the plane (due to time of travel) while your raycast actually hit the target. This may cause frustration / anger by players who see the bullet passing by but they still got hit. If you want to simulate a flying bullet, it may be better to just lerp it over time to the hitpoint of the ray. That way the bullet just stops at the hitpoint. Another solution would be to lerp it to the hitpoint which moves with the hit plane. It may look a bit strange since the bullet would turn into a homing bullet, but that's effectively what you have here since you have a hit-scan weapon which always hits.
A minor change that would prevent unnecessary garbage is to replace
if (hit.transform.tag == "Plane")
withif (hit.transform.CompareTag("Plane"))
because this doesn't allocate a new string each time it's called.The last thing would be to replace
hit.transform.gameObject.GetComponent
with justhit.transform.GetComponent
. Even internally Component.GetComponent does grab the gameobject of the component you call the method on, doing it manually won't give you any performance increase and just makes the line longer than it needs to be.
About the coroutine i've mentioned above, it can be simply done like this:
IEnumerator Shoot ()
{
while (true)
{
if(Input.GetKey(KeyCode.Space))
{
CmdShootBullet (shootLeftGun);
yield return new WaitForSeconds(0.5f);
}
yield return null; // this is important
}
}
void Start()
{
if (hasAuthority)
StartCoroutine(Shoot());
}
This has several advantages. First of all we only start the coroutine once and it keeps running. During the fire delay the input isn't even checked since the coroutine is waiting. Additionally all the other planes do not even start the coroutine and don't have to worry about checking input (or hasAuthority) every frame. You can (or should) remove the FixedUpdate and Update method(s) unless you need it for other things.
This could be further improved by putting the firing logic onto the server and let the client just tell the server if he's currently holding down the firekey or not. This would reduce the network traffic since you only need to send keydown / up events. It also prevents clients from calling "CmdShootBullet" a bazillion times per frame since the actual shooting delay is executed on the server.
Thankyou very much @Bunny83, when I get home I'll fully read this and try out any suggestions :)
Hey again @Bunny83 , I'm home from work now and have read your answer fully! Thanks for the useful insight into different problems I have with my code. I will change pieces of my code to what you've mentioned and see if I can fix this damage issue, I'll report back how I get on. Thanks once again :)