command, delegates and/or syncvar not updating clients values
Hey guys and gals,
I am struggling with syncing my client and server in a shooter. The most frustrating part is im following a tutorial and i swear I have done everything the instructor has (i have embellished extra features to make my game my own).
I have a health class (and a similar armor class) which are added as components to a player class, see code below. When the client shoots the server player everything updates fine, UI updates and everything, but when the server shoots the client player, the server has the correct values, however the client does not update. This applies to both the client player gui, but also, when I run the debug window in the Unity Editor as client, neither player has taken armor or health damage in their respective classes.
I'm not 100% comfortable with netowrking yet and I've tried debugging to understand the order of things. As you will see I have also placed a Debug.Log to see if TakeDamage() plays in the player class and one on OnHealthSynced (in health class). As a client OnHealthSynced triggers but not TakeDamage when the client takes damage. However Take damage triggers when the client harms the server.
Note: I have already gotten many other things to sync across the server so I cant see any faults with the network components such as the manager, gui, identifiers, spawning locations, animators etc.
I really hope someone has the time to look at this. Kinda stuck in the tutorial without it :D
Player script (I'm sorry I didn't dare to shorten it since I am struggling after all finding the mistake, however apart from the initialization, i believe most of the relevant calls are at the very bottom.)
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
using UnityStandardAssets.Characters.ThirdPerson;
[RequireComponent(typeof(Health))]
[RequireComponent(typeof(Armor))]
public class Player : NetworkBehaviour, IDamagable
{
public delegate void PlayerDiedDelegate();
public event PlayerDiedDelegate OnPlayerDied;
public enum PlayerTool
{
Axe,
ObstacleVertical,
ObstacleRamp,
ObstacleHorizontal,
Empty
}
[Header("Focal Point Variables")]
[SerializeField] private GameObject cameraTarget;
[SerializeField] private GameObject targetRotationObject;
[SerializeField] private float focalDistance;
[SerializeField] private float cameraChangeSideViewSpeed;
[SerializeField] private KeyCode toggleCameraSideViewKey;
[Header("Interaction")]
[SerializeField] private KeyCode interactionKey;
[SerializeField] private float interactionDistance;
[Header("Gameplay")]
[SerializeField] private PlayerTool tool;
[SerializeField] private KeyCode toolSwitchKey;
[SerializeField] private float resourceCollectionCooldown;
[SerializeField] private int startingResources;
[Header("Obstacle Placement")]
[SerializeField] private GameObject[] obstaclePrefabs;
[Header("Combat")]
[SerializeField] private KeyCode reloadKey;
[SerializeField] private int activeWeapon;
[SerializeField] private GameObject aimRaycastOrigin;
[SerializeField] private GameObject rocketPrefab;
[Header("Other")]
[SerializeField] private KeyCode pauseKey;
[SerializeField] private ThirdPersonUserControl tpuc;
[Header("Debug")]
[SerializeField] private GameObject debugImpactPositionPrefab;
private bool isRightSideView = true;
private bool isPaused = false;
private int resources;
private float resourceCollectionCooldownTimer = 0;
private GameObject currentObstacle;
private Transform lookingAtObject;
private List<Weapon> weapons;
private Weapon weapon;
private HUDController hud;
private GameCamera gameCamera;
private GameObject obstaclePlacementContainer;
private GameObject obstacleContainer;
private int obstacleToAddIndex;
private Health health;
private Armor armor;
// Start is called before the first frame update
void Start()
{
Cursor.lockState = CursorLockMode.Locked;
//Initialize values
resources = startingResources;
armor = GetComponent<Armor>();
armor.OnArmorChanged += OnArmorChanged;
weapons = new List<Weapon>();
health = GetComponent<Health>();
health.OnHealthChanged += OnHealthChanged;
//applies the camera and hud only if this is the local player
if (isLocalPlayer)
{
// Game Camera
gameCamera = FindObjectOfType<GameCamera>();
obstaclePlacementContainer = gameCamera.ObstaclePlacementContainer;
gameCamera.TargetLookAtObject = cameraTarget;
gameCamera.TargetRotationObject = targetRotationObject;
//Hud elements
hud = FindObjectOfType<HUDController>();
hud.SetActiveScreen("MainGameScreen");
hud.Resources = resources;
hud.UpdateHealthAndArmor(health, armor);
hud.Tool = tool;
hud.UpdateWeapon(null);
}
// Obstacle container
obstacleContainer = GameObject.Find("ObstacleContainer");
}
// Update is called once per frame
void Update()
{
//prevent update from happening to other players
if (!isLocalPlayer) return;
//toggle paused on player key
if (Input.GetKeyDown(pauseKey))
{
isPaused = !isPaused;
tpuc.IsPaused = isPaused;
if (isPaused)
{
hud.Pause();
} else
{
hud.Resume();
}
}
//check if paused or else play
if(!isPaused)
{
//update timers.
resourceCollectionCooldownTimer -= Time.deltaTime;
//camera stuff
if (Input.GetKeyDown(toggleCameraSideViewKey)) isRightSideView = !isRightSideView;
float targetX = focalDistance * (isRightSideView ? 1 : -1);
float smoothX = Mathf.Lerp(cameraTarget.transform.localPosition.x, targetX, Time.deltaTime * cameraChangeSideViewSpeed);
cameraTarget.transform.localPosition = new Vector3(smoothX, cameraTarget.transform.localPosition.y, cameraTarget.transform.localPosition.z);
//Interaction logic.
if (lookingAtObject != null && lookingAtObject.GetComponent<PickUpItem>()) lookingAtObject.GetComponent<PickUpItem>().ToggleItemHUD(false); // this clears the small pickupitem canvases
lookingAtObject = null;
RaycastHit hit;
if (Physics.Raycast(gameCamera.transform.position, gameCamera.transform.forward, out hit, interactionDistance))
{
//Looking At's
lookingAtObject = hit.transform;
//give hud message from looking at it.
if (hit.transform.GetComponent<Door>())
{
hud.GiveFeedback("Press E to open door.");
}
else if (hit.transform.GetComponent<PickUpItem>())
{
lookingAtObject.GetComponent<PickUpItem>().ToggleItemHUD(true);
}
else if (hit.transform.GetComponent<Chest>())
{
hud.GiveFeedback("Press E to open chest.");
}
// Interactions
if (Input.GetKeyDown(interactionKey))
{
if (hit.transform.GetComponent<Door>())
{
hit.transform.GetComponent<Door>().Interact();
}
else if (hit.transform.GetComponent<PickUpItem>())
{
PickUpItem pickUp = hit.transform.GetComponent<PickUpItem>();
bool didPickup = AddItem(pickUp.Type, pickUp.Amount);
if (didPickup) CmdPickUpItem(hit.transform.gameObject);
}
else if (hit.transform.GetComponent<Chest>())
{
CmdOpenChest(hit.transform.gameObject);
}
}
}
#if UNITY_EDITOR
//Draw interation line
Debug.DrawLine(gameCamera.transform.position, gameCamera.transform.position + gameCamera.transform.forward * interactionDistance, Color.green);
#endif
//select weapons
if (Input.GetKeyDown(KeyCode.Alpha1))
{
SwitchWeapon(0);
}
else if (Input.GetKeyDown(KeyCode.Alpha2))
{
SwitchWeapon(1);
}
else if (Input.GetKeyDown(KeyCode.Alpha3))
{
SwitchWeapon(2);
}
else if (Input.GetKeyDown(KeyCode.Alpha4))
{
SwitchWeapon(3);
}
else if (Input.GetKeyDown(KeyCode.Alpha5))
{
SwitchWeapon(4);
}
// Weapon
UpdateWeapon();
// Tool switch logic
if (Input.GetKeyDown(toolSwitchKey))
{
SwitchTool();
}
// Preserving the obstacles horizontal rotation.
if (currentObstacle != null)
{
currentObstacle.transform.eulerAngles = new Vector3(
0,
currentObstacle.transform.eulerAngles.y,
currentObstacle.transform.eulerAngles.z);
}
// Tool usage logic (continuous).
if (Input.GetAxis("Fire1") > 0)
{
UseToolContinuous();
}
//tool usage logic (trigger)
if (Input.GetMouseButtonDown(0))
{
UseToolTrigger();
}
}
}
[Command]
void CmdOpenChest(GameObject lookingAtObject)
{
lookingAtObject.GetComponent<Chest>().Interact();
}
[Command]
void CmdPickUpItem(GameObject pickUp)
{
pickUp.GetComponent<PickUpItem>().Interact();
}
private void SwitchWeapon(int index)
{
if(index < weapons.Count)
{
weapon = weapons[index];
hud.UpdateWeapon(weapon);
//force tool to be none
tool = PlayerTool.Empty;
hud.Tool = tool;
if (currentObstacle) Destroy(currentObstacle);
//Zoom out
gameCamera.ZoomOut();
hud.SniperAimVisibility = false;
}
else { hud.GiveFeedback("No weapon equipped in slot"); }
}
private void SwitchTool()
{
//clear weapon
weapon = null;
hud.UpdateWeapon(weapon);
//zoom the camera out
gameCamera.ZoomOut();
hud.SniperAimVisibility = false;
// cycle between the available tools
int currentToolIndex = (int)tool; //converts from Enum of tool to int value
currentToolIndex++;
if (currentToolIndex == System.Enum.GetNames(typeof(PlayerTool)).Length)
{
currentToolIndex = 0;
}
tool = (PlayerTool)currentToolIndex;
hud.Tool = tool;
// check for obstacle placement logic
obstacleToAddIndex = -1;
if (tool == PlayerTool.ObstacleVertical) obstacleToAddIndex = 0;
else if (tool == PlayerTool.ObstacleRamp) obstacleToAddIndex = 1;
else if (tool == PlayerTool.ObstacleHorizontal) obstacleToAddIndex = 2;
//show object in placement mode
if (currentObstacle != null) Destroy(currentObstacle);
if (obstacleToAddIndex >= 0)
{
currentObstacle = Instantiate(obstaclePrefabs[obstacleToAddIndex]);
currentObstacle.transform.SetParent(obstaclePlacementContainer.transform);
currentObstacle.transform.localPosition = Vector3.zero;
currentObstacle.transform.localRotation = Quaternion.identity;
currentObstacle.GetComponent<Obstacle>().SetPositioningMode();
//update required text in hud
hud.UpdateResourcesRequirement(currentObstacle.GetComponent<Obstacle>().Cost, resources);
}
}
private void UseToolContinuous()
{
if (tool == PlayerTool.Axe)
{
RaycastHit hit;
if (Physics.Raycast(gameCamera.transform.position, gameCamera.transform.forward, out hit, interactionDistance))
{
// if hitting a resource object with axe
if (resourceCollectionCooldownTimer <= 0 && hit.transform.GetComponent<ResourceObject>())
{
resourceCollectionCooldownTimer = resourceCollectionCooldown;
ResourceObject resourceObject = hit.transform.GetComponent<ResourceObject>();
resources += resourceObject.TakeDamage(1);
hud.Resources = resources;
}
}
}
}
private void UseToolTrigger()
{
if (currentObstacle != null)
{
Obstacle obst = currentObstacle.GetComponent<Obstacle>();
if (obst.Cost <= resources)
{
resources -= currentObstacle.GetComponent<Obstacle>().Cost;
CmdPlaceObstacle(obstacleToAddIndex, currentObstacle.transform.position, currentObstacle.transform.rotation);
//update required text in hud
hud.UpdateResourcesRequirement(obst.Cost, resources);
hud.Resources = resources;
}
else
{
//Give cost warning!
hud.GiveFeedback("Not enough resources!");
}
}
}
[Command]
void CmdPlaceObstacle(int index, Vector3 position, Quaternion rotation)
{
GameObject newObstacle = Instantiate(obstaclePrefabs[index]);
newObstacle.transform.SetParent(obstacleContainer.transform);
newObstacle.transform.position = position;
newObstacle.transform.rotation = rotation;
currentObstacle.GetComponent<Obstacle>().Place();
NetworkServer.Spawn(newObstacle);
}
private void OnTriggerEnter(Collider otherCollider)
{
if (!isLocalPlayer)
{
return;
}
}
private bool AddItem(PickUpItem.ItemType type, int amount)
{
//Create a weapon reference
Weapon currentWeapon = null;
bool alreadyHaveWeapon = false;
//Check if we have an instance of the weapon
for (int i = 0; i< weapons.Count; i++)
{
if(type == PickUpItem.ItemType.Pistol && weapons[i] is Pistol)
{
currentWeapon = weapons[i];
alreadyHaveWeapon = true;
}
else if (type == PickUpItem.ItemType.MachineGun && weapons[i] is MachineGun)
{
currentWeapon = weapons[i];
alreadyHaveWeapon = true;
}
else if (type == PickUpItem.ItemType.Shotgun && weapons[i] is Shotgun)
{
currentWeapon = weapons[i];
alreadyHaveWeapon = true;
}
else if (type == PickUpItem.ItemType.Sniper && weapons[i] is Sniper)
{
currentWeapon = weapons[i];
alreadyHaveWeapon = true;
}
else if (type == PickUpItem.ItemType.RocketLauncher && weapons[i] is RocketLauncher)
{
currentWeapon = weapons[i];
alreadyHaveWeapon = true;
}
}
//if we dont have a weapon of this type, create one.
if(currentWeapon == null)
{
if (type == PickUpItem.ItemType.Pistol) currentWeapon = new Pistol();
else if (type == PickUpItem.ItemType.MachineGun) currentWeapon = new MachineGun();
else if (type == PickUpItem.ItemType.Shotgun) currentWeapon = new Shotgun();
else if (type == PickUpItem.ItemType.Sniper) currentWeapon = new Sniper();
else if (type == PickUpItem.ItemType.RocketLauncher) currentWeapon = new RocketLauncher();
weapons.Add(currentWeapon);
}
//attempt ammo pickup
bool fullAmmo = currentWeapon.AddAmmunition(amount);
// if already full give feedback and abort pickup
if (fullAmmo)
{
hud.GiveFeedback("Already full of ammo!");
return false;
}
else
{
if(!alreadyHaveWeapon) currentWeapon.LoadClip();
if(currentWeapon == weapon) hud.UpdateWeapon(weapon);
return true; // complete pickup
}
}
private void UpdateWeapon()
{
if (weapon != null) {
//Reloading
if (Input.GetKeyDown(reloadKey))
{
bool hasReloaded = weapon.Reload();
if (!hasReloaded) hud.GiveFeedback("Already Reloading!");
else
{
hud.GiveFeedback("Reloading...");
// TODO: reloading animation
}
hud.UpdateWeapon(weapon);
}
//Shooting
float timeElapsed = Time.deltaTime;
bool isPressingTrigger = Input.GetAxis("Fire1")>0.1f;
bool hasShot = weapon.Update(timeElapsed, isPressingTrigger);
hud.UpdateWeapon(weapon);
if (hasShot)
{
Shoot();
}
//Zoom logic
if (weapon != null && Input.GetMouseButtonDown(1))
{
//triggers zoom and returns if result is zoomed
bool isZoomed = gameCamera.TriggerZoom(weapon.WeaponFOV);
//sets hud sniper mode if both zoomed and in snipermode
hud.SniperAimVisibility = (isZoomed && weapon is Sniper);
}
}
}
private void Shoot()
{
//set no of simultanteous projectiles to 1 as guns usually shoot only one at a time
int amountOfBullets = 1;
// some weapons will have more than one projectiles at once (i.e. shotguns)
if(weapon is Shotgun)
{
amountOfBullets = ((Shotgun)weapon).BulletSwarmQty;
}
//for each projectile calculate hits
for(int i =0; i< amountOfBullets; i++)
{
//calculate players aim
float distanceFromCamera = Vector3.Distance(gameCamera.transform.position, transform.position);
RaycastHit targetHit;
if(Physics.Raycast(gameCamera.transform.position + gameCamera.transform.forward * distanceFromCamera, gameCamera.transform.forward, out targetHit))
{
Vector3 hitPosition = targetHit.point;
//second raycast which calculates projectile spread
Vector3 shootDirection = (hitPosition - aimRaycastOrigin.transform.position).normalized;
shootDirection = new Vector3(
shootDirection.x+ UnityEngine.Random.Range(-weapon.AimVariation, weapon.AimVariation),
shootDirection.y+ UnityEngine.Random.Range(-weapon.AimVariation, weapon.AimVariation),
shootDirection.z+ UnityEngine.Random.Range(-weapon.AimVariation, weapon.AimVariation)
);
shootDirection.Normalize();
//this section is for all hitScan weapons
if(!(weapon is RocketLauncher))
{
RaycastHit shootHit;
if (Physics.Raycast(aimRaycastOrigin.transform.position, shootDirection, out shootHit)){
GameObject debugPositionInstance = Instantiate(debugImpactPositionPrefab);
debugPositionInstance.transform.position = shootHit.point;
Destroy(debugPositionInstance, 0.5f);
if (shootHit.transform.GetComponent<IDamagable>() != null)
CmdDamage(shootHit.transform.gameObject, weapon.Damage);
else if (shootHit.transform.GetComponentInParent<IDamagable>() != null)
CmdDamage(shootHit.transform.parent.gameObject, weapon.Damage);
#if UNITY_EDITOR
//Draw shooting line
Debug.DrawLine(aimRaycastOrigin.transform.position, aimRaycastOrigin.transform.position + shootDirection * 1000, Color.red);
#endif
}
}
else // weapon is a rocket launcher
{
GameObject rocket = Instantiate(rocketPrefab);
rocket.transform.position = aimRaycastOrigin.transform.position + shootDirection;
rocket.GetComponent<Rocket>().Launch(shootDirection);
//notice there is no transformation of damage here as it is calculated by the explosion.
}
}
}
}
[Command]
private void CmdDamage(GameObject target, float amount)
{
Debug.Log("CMDDamage triggeret on target: " + target.name + " with damage " + amount);
if(target) target.GetComponent<IDamagable>().TakeDamage(amount);
}
public int TakeDamage(float amount)
{
Debug.Log("Player takes " + amount + " damage");
//disperses damage to armor first. Left over damages health
float damageLeft = Mathf.Max(0, amount-armor.Value);
Debug.Log("Armor takes: "+Mathf.Min(amount, armor.Value) + " damage. " + Mathf.Max(0, armor.Value-amount) +" armor left. Player takes " + damageLeft + " in health damage.");
armor.Damage(amount);
health.Damage(damageLeft);
return 0;
}
private void OnHealthChanged(float newHealthValue)
{
//only applies this method on local player
if (!isLocalPlayer) return;
// Update health and Armor
hud.UpdateHealthAndArmor(health, armor);
if (newHealthValue <= 0.0f)
{
//you die, swap game gui screen
Debug.Log("You Die");
hud.Dead();
Cursor.lockState = CursorLockMode.None;
OnPlayerDied?.Invoke();
CmdDestroy();
}
}
[Command]
private void CmdDestroy()
{
//Coarse death solution. TODO make elegant
Destroy(gameObject);
}
private void OnArmorChanged(float newArmor)
{
//this method only applies to local player
if (!isLocalPlayer) return;
// Update health and Armor
hud.UpdateHealthAndArmor(health, armor);
}
}
Health Class (Armor class looks exact the same but with "armor" instead)
using UnityEngine;
using UnityEngine.Networking;
public class Health : NetworkBehaviour
{
// Event
public delegate void HealthChangedDelegate(float health);
public event HealthChangedDelegate OnHealthChanged;
//Health
private const float defaultHealth = 100;
[SyncVar(hook = "OnHealthSynced")]
private float health = defaultHealth;
//Properties
public float Value { get { return health; } }
public float MaxValue { get { return defaultHealth; } }
public void Damage ( float amount)
{
health -= amount;
if (health < 0) health = 0;
}
private void OnHealthSynced(float newHealth)
{
Debug.Log("OnhealthSynced is triggerd!");
if (OnHealthChanged != null) OnHealthChanged(newHealth);
}
}