Multiplayer Skill System with Behavior Designer
Published 10 months ago
Behavior Designer is not only for AI
This article is for developers, who are familiar with Behavior Design, UNET and have solid knowledge of C#. This is just an example.
First of all, this may not be the best way, but at least it can be clear for someone to start. The system I made works fine with Behavior Designer and in multiplayer. It also works only when multiplayer game is active, i.e. prepare a scene with Network Manager.
To start, lets create simple C# classes. It will be Skill.cs and SkillIcon.cs. Have a look on the code below. Its description is right under:
using UnityEngine; using UnityEngine.Networking; public class Skill : NetworkBehaviour { public string SkillName; public BehaviorDesigner.Runtime.BehaviorTree Tree; public SkillIcon skillIcon; public float CoolDown = 5; float NextAvailableTime; [SyncVar(hook = "OnAvailabilityUpdated")] public float Availability; static readonly Color SelectedColor = new Color(255, 255, 255, 1); static readonly Color NotSelectedColor = new Color(255, 255, 255, 0.5f); void Start() { if ( NextAvailableTime = Time.timeSinceLevelLoad + CoolDown; } void Update() { if ( UpdateAvailability(); } public void Init() { skillIcon.SetSkill(this); } public void Cast(GameObject Target, GameObject Source) { Tree.enabled = true; Tree.SetVariableValue("TargetController", Target); Tree.SetVariableValue("SourceController", Source); if ( NextAvailableTime = Time.timeSinceLevelLoad + CoolDown; } public bool isAvailable() { return Availability == 1; } public float GetTimeLeft() { return NextAvailableTime - Time.timeSinceLevelLoad; } /// <summary> /// Updates availabilty of skill. Have to be called on server. /// </summary> public void UpdateAvailability() { Availability = Mathf.Clamp01(1 - (GetTimeLeft() / CoolDown)); } //Clients recives this method call public void OnAvailabilityUpdated(float availability) { Availability = availability; } public void SetSelected(bool Selected) { skillIcon.image.color = Selected ? SelectedColor : NotSelectedColor; } }
This class provides common properties and functionality of all skills. The most important method to notices is Cast method. It accepts two parameters Source and Target. This two are always important to have, when a player is casting something. Then it sets variable in a Tree. Then it enables it to let to start the effect. Alternatively, you can replace it with your class, that handles effect of a skill. In my case, I assume that BehaviorTree is always set to StartWhenEnabled and by default all Tree Skills components are disabled. They also have to contain a task that disables them again. Have a look on example:
Following Tree, "spawns" fake item from the list to confuse another player. Also notice that I want this happen only on server, i.e. moving part. The reason is that FakePackage has NetworkTransform component on it. And only server can send the current position of FakePackage, because Authority it given to local player .
Here the second script to use:
using UnityEngine; public class SkillIcon : MonoBehaviour { [HideInInspector] public UnityEngine.UI.Image image; Skill skill; void Update() { if (skill != null) { image.fillAmount = Mathf.Clamp01(skill.Availability); } } public void SetSkill(Skill newSkill) { image = GetComponent<UnityEngine.UI.Image>(); skill = newSkill; } }
It is only for the UI.
Let me also show you the way of triggering Skill class from for example Player controller:
using UnityEngine; using UnityEngine.Networking; public class PlayerController : NetworkBehaviour { [SyncVar(hook = "OnSelectedSkillChanged")] public int SelectedSkill; [HideInInspector] public Skill[] Skills; public delegate void CastDelegate(); [SyncEvent] public event CastDelegate EventCast; void Start() { GameObject[] MySkills = GameObject.FindGameObjectsWithTag(tag + "Skills"); Skills = new Skill[MySkills.Length]; for (int i = 0; i < MySkills.Length; i++) { Skills[i] = MySkills[i].GetComponent<Skill>(); Skills[i].skillIcon.gameObject.SetActive(hasAuthority); Skills[i].Init(); } EventCast += Cast; } void Update() { if(!hasAuthority) return; for (int i = 48; i <= 57; i++) { if (Input.GetKeyDown((KeyCode)i) && Skills[i - 49] != null) { CmdSetSkill(Mathf.Clamp(i - 49, 0, Skills.Length - 1)); } } if (Input.GetMouseButtonDown(0)) if (Skills[SelectedSkill] != null && Skills[SelectedSkill].isAvailable()) CmdCast(); } [Command] public void CmdSetSkill(int newSkill) { SelectedSkill = newSkill; } public void OnSelectedSkillChanged(int newSkill) { Skills[SelectedSkill].SetSelected(false); SelectedSkill = newSkill; Skills[SelectedSkill].SetSelected(true); } [Command] public void CmdCast() { EventCast(); } public void Cast() { Target = GameObject.FindWithTag(CompareTag("Pechkin") ? "Fedor" : "Pechkin"); //Debug.Log("[PlayerController] " + NickName + " trying to cast " + Skills[SelectedSkill].SkillName); Skills[SelectedSkill].Cast(Target, gameObject); #if UNITY_EDITOR if (!Target) Debug.LogError("[PlayerController] No target found"); #endif } }
Now, what the hell is going here?😅 Don't be afraid, it is simple) On Start the object looks for corresponding skills. It search them by tag+"Skills". The reason I use tag is because skills exists on a scene. Their tag can be "EnemySkills" or "InsertHereCharacterNameSkills". Then it gets component from them and place in array to be accessible using Alpha number on keyboard.
It also assign method Cast to a SyncEvent variable, because I want the skill to be triggered on all clients. One way to do it is to ask a server to Trigger SyncEvent. As a result the method is called everywhere, but how does it know which skill to use? On Update it checks all alpha buttons using for loop. Once on of them is pressed (down) it asks server to updated selected skill and then all Clients receives new index of selected skill, because of hook on SyncVar.
The "firing" happens, when a player presses LMB. It actually, asks a server to fire SyncEvent. Then the cast of specific player happens on clients.

What if some effects have be to visible only to Source or Target?

For this you can use hasAuthority task in your Behavior Tree. Simply add this class in a right place:
using UnityEngine.Networking; namespace BehaviorDesigner.Runtime.Tasks.Basic.UnityNetwork { [TaskCategory("Network")] [TaskDescription("Checks if TargetNetworkIdentity has authority.")] public class HasAuthority : Conditional { public SharedGameObject TargetNetworkIdentity; NetworkIdentity netId; public override void OnStart() { if (TargetNetworkIdentity.Value) netId = TargetNetworkIdentity.Value.GetComponent<NetworkIdentity>(); } public override TaskStatus OnUpdate() { if (!netId) return TaskStatus.Failure; return netId.hasAuthority ? TaskStatus.Success : TaskStatus.Failure; } } }
I hope everything was clear😊 Otherwise, let me know in the comments what you need help with.
Indie Game Dev - Executive