This game is tested on a “localhost” IP on the same computer. Left is the client, right is the server. Sorry for not being able to make the screenshot more apparent.
This is what happens:
Order of operations as follows:
- Player selects 1 unit.
- Player orders “Split” command.
- Unit game object calls on [Command], which calls on [ClientRpc].
- The [ClientRpc] contains an action where the Unit game object is instantiated on itself, creating a copy.
- The Unit game object and the Copy is then put into a struct.
- This struct is then added into a splitting manager’s list of structs.
- We leave the [ClientRpc]. The server and the client now have non-empty split managers.
- If the splitting manager’s list of structs is not empty, update each struct in the list.
- The struct containing the unit and the copy finally finishes. Struct is deleted.
- For unknown reasons, the values in either the original unit or the copy are corrupted.
This is all I know of so far.
This is the code for the Split Manager:
public void Update() {
if (!this.hasAuthority) {
return;
}
//When the player starts the action to split a game unit into two, it takes in all the selected game units
//one by one, and splits them individually.
if (Input.GetKeyDown(KeyCode.S)) {
if (this.selectionManager != null) {
AddingNewSplitGroup(); //Just adding new groups to the list.
}
}
UpdateSplitGroup(); //Just updating any groups inside the list.
}
public void UpdateSplitGroup() {
if (this.splitGroupList != null && this.splitGroupList.Count > 0) {
for (int i = 0; i < this.splitGroupList.Count; i++) {
SplitGroup group = this.splitGroupList[i];
if (group.elapsedTime > 1f) {
group.Stop();
if (group.splitUnit != null && !this.selectionManager.allObjects.Contains(group.splitUnit.gameObject)) {
this.selectionManager.allObjects.Add(group.splitUnit.gameObject);
}
if (!this.selectionManager.allObjects.Contains(group.ownerUnit.gameObject)) {
this.selectionManager.allObjects.Add(group.ownerUnit.gameObject);
}
this.removeList.Add(group); //Nothing is involved in modifying unit attributes.
}
else {
//Some weird C# language design...
group.Update();
group.elapsedTime += Time.deltaTime / group.splitFactor;
this.splitGroupList[i] = group;
}
}
}
if (this.removeList != null && this.removeList.Count > 0) {
foreach (SplitGroup group in this.removeList) {
this.splitGroupList.Remove(group);
}
this.removeList.Clear();
}
}
private void AddingNewSplitGroup() {
foreach (GameObject obj in this.selectionManager.selectedObjects) {
if (obj == null) {
this.selectionManager.removeList.Add(obj);
continue;
}
GameUnit objUnit = obj.GetComponent<GameUnit>();
if (objUnit.level == 1) {
CmdSplit(obj, objUnit.hasAuthority);
}
}
return;
}
This is the network code for Split Manager. I swear I’m pretty sure data corruptions happen in this code snippet, but I can’t tell for sure.
[Command]
public void CmdSplit(GameObject obj, bool hasAuthority) {
GameUnit unit = obj.GetComponent<GameUnit>();
if (unit.attributes == null) {
if (this.unitAttributes != null) {
unit.attributes = this.unitAttributes;
}
else {
Debug.LogError("Definitely something is wrong here with unit attributes.");
}
}
if (unit.isSplitting) {
return;
}
//This is profoundly one of the hardest puzzles I had tackled. Non-player object spawning non-player object.
//Instead of the usual spawning design used in the Spawner script, the spawning codes here are swapped around.
//In Spawner, you would called on NetworkServer.SpawnWithClientAuthority() in the [ClientRpc]. Here, it's in [Command].
//I am guessing it has to do with how player objects and non-player objects interact with UNET.
GameObject split = MonoBehaviour.Instantiate(this.gameUnitPrefab) as GameObject;
split.transform.position = obj.transform.position;
GameUnit splitUnit = split.GetComponent<GameUnit>();
if (splitUnit != null) {
splitUnit.isSplitting = unit.isSplitting = true;
}
NetworkIdentity managerIdentity = this.GetComponent<NetworkIdentity>();
NetworkServer.SpawnWithClientAuthority(split, managerIdentity.clientAuthorityOwner);
float angle = UnityEngine.Random.Range(-180f, 180f);
RpcSplit(obj, split, angle, hasAuthority, this.unitAttributes.splitPrefabFactor);
}
[ClientRpc]
public void RpcSplit(GameObject obj, GameObject split, float angle, bool hasAuthority, float splitFactor) {
//We do not call on NetworkServer methods here. This is used only to sync up with the original game unit for all clients.
//This includes adding the newly spawned game unit into the Selection Manager that handles keeping track of all game units.
GameUnit original = obj.GetComponent<GameUnit>();
GameUnit copy = split.GetComponent<GameUnit>();
Copy(original, copy);
NavMeshAgent originalAgent = obj.GetComponent<NavMeshAgent>();
originalAgent.ResetPath();
NavMeshAgent copyAgent = split.GetComponent<NavMeshAgent>();
copyAgent.ResetPath();
GameObject[] splitManagerGroup = GameObject.FindGameObjectsWithTag("SplitManager");
if (splitManagerGroup.Length > 0) {
for (int i = 0; i < splitManagerGroup.Length; i++) {
SplitManager manager = splitManagerGroup[i].GetComponent<SplitManager>();
if (manager != null && manager.hasAuthority == hasAuthority) {
manager.splitGroupList.Add(new SplitGroup(original, copy, angle, splitFactor));
if (manager.selectionManager == null) {
GameObject[] objs = GameObject.FindGameObjectsWithTag("SelectionManager");
foreach (GameObject select in objs) {
SelectionManager selectManager = select.GetComponent<SelectionManager>();
if (selectManager.hasAuthority) {
manager.selectionManager = selectManager;
}
}
}
manager.selectionManager.allObjects.Add(split);
}
}
}
}
private static void Copy(GameUnit original, GameUnit copy) {
copy.isSelected = original.isSelected;
copy.isSplitting = original.isSplitting;
copy.isMerging = original.isMerging;
copy.transform.position = original.transform.position;
copy.transform.rotation = original.transform.rotation;
copy.transform.localScale = original.transform.localScale;
copy.oldTargetPosition = original.oldTargetPosition = -Vector3.one * 9999f;
copy.isDirected = original.isDirected = false;
copy.level = original.level;
copy.previousLevel = original.previousLevel;
copy.maxHealth = original.maxHealth;
copy.currentHealth = original.currentHealth;
if (copy.currentHealth > copy.maxHealth) {
copy.currentHealth = copy.maxHealth;
}
if (original.currentHealth > original.maxHealth) {
original.currentHealth = original.maxHealth;
}
copy.recoverCooldown = original.recoverCooldown;
copy.recoverCounter = original.recoverCounter = 0;
copy.speed = original.speed;
copy.attackCooldown = original.attackCooldown;
copy.attackCooldownCounter = original.attackCooldownCounter = 0;
copy.attackPower = original.attackPower;
copy.attributes = original.attributes;
copy.teamColorValue = original.teamColorValue;
original.SetTeamColor(original.teamColorValue);
copy.SetTeamColor(copy.teamColorValue);
}
And this is the Split Group struct the Split Manager handles:
[System.Serializable]
public struct SplitGroup {
public GameUnit ownerUnit;
public GameUnit splitUnit;
public float elapsedTime;
public Vector3 rotationVector;
public float splitFactor;
public Vector3 origin;
public SplitGroup(GameUnit ownerUnit, GameUnit splitUnit, float angle, float splitFactor) {
this.ownerUnit = ownerUnit;
this.splitUnit = splitUnit;
this.elapsedTime = 0f;
this.origin = ownerUnit.gameObject.transform.position;
this.splitFactor = splitFactor;
SpawnRange range = this.ownerUnit.GetComponentInChildren<SpawnRange>();
this.rotationVector = Quaternion.Euler(0f, angle, 0f) * (Vector3.one * range.radius);
NavMeshAgent agent = this.ownerUnit.GetComponent<NavMeshAgent>();
if (agent != null) {
agent.ResetPath();
agent.Stop();
}
agent = this.splitUnit.GetComponent<NavMeshAgent>();
if (agent != null) {
agent.ResetPath();
agent.Stop();
}
NetworkTransform transform = this.ownerUnit.GetComponent<NetworkTransform>();
if (transform != null) {
transform.transformSyncMode = NetworkTransform.TransformSyncMode.SyncNone;
}
transform = this.splitUnit.GetComponent<NetworkTransform>();
if (transform != null) {
transform.transformSyncMode = NetworkTransform.TransformSyncMode.SyncNone;
}
}
//Again, nothing is involved in modifying anything related to the units. Current health, max health, nothing is involved.
public void Update() {
this.ownerUnit.isSelected = false;
this.splitUnit.isSelected = false;
Vector3 pos = Vector3.Lerp(this.origin, this.origin + this.rotationVector, this.elapsedTime);
if (this.ownerUnit == null || this.ownerUnit.gameObject == null) {
this.elapsedTime = 1f;
return;
}
this.ownerUnit.gameObject.transform.position = pos;
pos = Vector3.Lerp(this.origin, this.origin - this.rotationVector, this.elapsedTime);
if (this.splitUnit == null || this.splitUnit.gameObject == null) {
this.elapsedTime = 1f;
return;
}
this.splitUnit.gameObject.transform.position = pos;
}
public void Stop() {
NavMeshAgent agent = null;
if (this.ownerUnit != null) {
this.ownerUnit.isSplitting = false;
agent = this.ownerUnit.GetComponent<NavMeshAgent>();
if (agent != null) {
agent.Resume();
}
}
if (this.splitUnit != null) {
this.splitUnit.isSplitting = false;
agent = this.splitUnit.GetComponent<NavMeshAgent>();
if (agent != null) {
agent.Resume();
}
}
NetworkTransform transform = this.ownerUnit.GetComponent<NetworkTransform>();
if (transform != null) {
transform.transformSyncMode = NetworkTransform.TransformSyncMode.SyncTransform;
}
transform = this.splitUnit.GetComponent<NetworkTransform>();
if (transform != null) {
transform.transformSyncMode = NetworkTransform.TransformSyncMode.SyncTransform;
}
}
};
I wanted to know what’s the best way to prevent data corruption due to [SyncVar]. I figured if someone else takes a look at the codes, maybe I could’ve missed out on something, or hinted that I’m doing something wrong with my [SyncVars]?
I know only the [Commands] can modify SyncVars on the servers and have the variables be synced across the network to the clients. But I just don’t see how the clients received mangled data values from the server, if the connection is “localhost”.