Game Definition:
I am creating a game involving spawning multiple objects (foods) at random places. The food will be destroyed when the player touches it. The number of foods will be more than 2000.
Problem:
I want these foods to show in all of the players' game environments. I am instantiating it from the Master, and all foods are using Photon View ID; however, the limit of ViewID is only 999. I tried increasing the maximum, but I am worried that it will cause problems like bandwidth issues.
Is there any way where I can synchronize the foods to all the players without using a lot of ViewID?
Create your own network ID and manager!
Depending on your needs the simplest thing would be to have a central manager (MasterClient) spawning food instances and assign them a unique ID. Then tell all other clients to also spawn this item and assign the same ID (e.g. using RPCs with all required parameters). Additionally for handling switching of MasterClient keep a list of all existing IDs e.g. in the Room properties so in case of a switch the new masterclient can take over the job to assign unique IDs => No limits ;)
Of course this can get quite "hacky" and you have to play around a bit and test it really well!
Note: The following code is untested and typed on a smartphone! But I hope it gives you a good starting point.
This class would go onto the Food prefab so every food has this custom network identity
// Put this on your food prefab(s)
public class FoodID : MonoBehaviour
{
// The assigned ID
public uint ID;
// An event to handle any kind of destroyed food no matter for what reason
// in general though rather go via the FoodManagement.DestroyFood method instead
public static event Action<FoodID> onDestroyed;
private void OnDestroy()
{
onDestroyed?.Invoke(this);
}
}
and this would go onto your player or into the scene so your other scripts can communicate with it and it has the authority to send RPCs around ;)
public class FoodManagement : MonoBehaviourPunCallbacks
{
[FormerlySerializedAs("foodPrefab")]
public FoodID foodIDPrefab;
// keep track of already ued IDs
private readonly HashSet<uint> _usedIDs = new HashSet<uint>
{
// by default I always block the 0 because it means invalid/unassigned ID ;)
0
};
// keep references from ID to food LOCAL
private readonly Dictionary<uint, FoodID> _foodInstances = new Dictionary<uint, FoodID>();
// instance for random number generation used in GetRandomUInt
private readonly Random _random = new Random();
private void Awake()
{
// Register a callback just to be sure that all kind of Destroy on a Food object is handled forwarded correctly
FoodID.onDestroyed += DestroyFood;
}
private void OnDestroy()
{
// In general make sure to remove callbacks once not needed anymore to avoid exceptions
FoodID.onDestroyed -= DestroyFood;
}
// Register a food instance and according ID to the dictionary and hashset
private void AddFoodInstance(FoodID foodID)
{
_usedIDs.Add(foodID.ID);
_foodInstances.Add(foodID.ID, foodID);
}
// Unregister a foo instance and according ID from the dictionary and hashset
private void RemoveFoodInstance(uint id)
{
_usedIDs.Remove(id);
_foodInstances.Remove(id);
}
// Get a unique random uint ID that is not already in use
private uint GetFreeID()
{
uint id;
do
{
id = GetRandomUInt();
} while (id == 0 || _usedIDs.Contains(id));
return id;
}
// Generates a random uint
private uint GetRandomUInt()
{
var thirtyBits = (uint)_random.Next(1 << 30);
var twoBits = (uint)_random.Next(1 << 2);
var fullRange = (thirtyBits << 2) | twoBits;
return fullRange;
}
// Create a new Food instance network wide on the given location
public void SpawnFood(Vector3 position)
{
// Make sure only the current Master client creates unique IDs in order to get no conflicts
if (PhotonNetwork.IsMasterClient)
{
SpawnFoodOnMaster(position);
}
else
{
photonView.RPC(nameof(SpawnFoodOnMaster), RpcTarget.MasterClient, position);
}
}
// Only the master client creates IDs and forwards th spawning to all clients
private void SpawnFoodOnMaster(Vector3 position)
{
if (!PhotonNetwork.IsMasterClient)
{
Debug.LogError($"{nameof(SpawnFoodOnMaster)} invoked on Non-Master client!");
return;
}
var id = GetFreeID();
photonView.RPC(nameof(RPCSpawnFood), RpcTarget.All, id, position);
}
// Finally all clients will spawn the food at given location and register it in their local ID registry
private void RPCSpawnFood(uint id, Vector3 position)
{
var newFood = Instantiate(foodIDPrefab, position, Quaternion.identity);
newFood.ID = id;
AddFoodInstance(newFood);
}
// Destroy the given Food network wide
public void DestroyFood(FoodID foodID)
{
DestroyFood(foodID.ID);
}
// Destroy the Food with given ID network wide
public void DestroyFood(uint id)
{
if (PhotonNetwork.IsMasterClient)
{
DestroyFoodOnMaster(id);
}
else
{
photonView.RPC(nameof(DestroyFoodOnMaster), RpcTarget.MasterClient, id);
}
}
// The same as for the spawning: Only the master client forwards this call
// Reason: This prevents conflicts if at the same time food is destroyed and created or
// if two clients try to destroy the same food at the same time
void DestroyFoodOnMaster(uint id)
{
if (!_usedIDs.Contains(id))
{
Debug.LogError($"Trying to destroy food with non-registered ID {id}");
return;
}
photonView.RPC(nameof(RPCDestroyFood), RpcTarget.All, id);
}
// Destroy Food ith given id network wide and remove it from the registries
void RPCDestroyFood(uint id)
{
if (_foodInstances.TryGetValue(id, out var food))
{
if (food) Destroy(food.gameObject);
}
RemoveFoodInstance(id);
}
// Once you join a new room make sure you receive the current state
// since our custom ID system is not automatically handled by Photon anymore
public override void OnJoinedRoom()
{
base.OnJoinedRoom();
if (PhotonNetwork.IsMasterClient) return;
photonView.RPC(nameof(RequestInitialStateFromMaster), RpcTarget.MasterClient, PhotonNetwork.LocalPlayer);
}
// When a new joined clients requests the current state as the master client answer with he current state
private void RequestInitialStateFromMaster(Player requester)
{
if (!PhotonNetwork.IsMasterClient)
{
Debug.LogError($"{nameof(RequestInitialStateFromMaster)} invoked on Non-Master client!");
return;
}
var state = _foodInstances.Values.ToDictionary(food => food.ID, food => food.transform.position);
photonView.RPC(nameof(AnswerInitialState), requester, state);
}
// When the master sends us the current state instantiate and register all Food instances
private void AnswerInitialState(Dictionary<uint, Vector3> state)
{
foreach (var kvp in state)
{
RPCSpawnFood(kvp.Key, kvp.Value);
}
}
}
Related
I have three buttons, three players, and I want to know which player clicked on which button by assigning them a different popup UI color. Let's say player 1- red, player 2 - green, player 3 - white. So when player 1 clicks on button(1), the red panel will be visible for all other players telling them who did that.
Possible solution: I have been advised to call to the Host with a Cmd method and pass the connectionId of the player who click on it. Then check the list of players on the NetworkManager, and find the matching player object. Once it has that player object, it would get the name and Id, and calls a RPC to the clients to show the UI element, passing them the correct text and color.
What I Have: I have list of my players:
public List<MyNetworkPlayer> players { get; } = new List<MyNetworkPlayer>();
and i can use my player by
MyNetworkPlayer player = conn.identity.GetComponent<MyNetworkPlayer>();
I also can attatch my NetworkManager if that can help:
public class MyNetworkManager : NetworkManager
{
public static event Action ClientOnConnected;
public static event Action ClientOnDisconnected;
// players should't be able to join the lobby during the game.
private bool isGameInProgress = false;
// here, we create a list of players, in order to display them in the lobby later.
public List<MyNetworkPlayer> players { get; } = new List<MyNetworkPlayer>();
#region Serwer
public override void OnServerConnect(NetworkConnection conn )
{ //kick a player if the game is in progress
if (!isGameInProgress) { return; }
base.OnServerConnect (conn);
conn.Disconnect();
}
// here i write some own logic about what heppens when a client connects to a server. If the connection is successful
// the message about connection will be display in a console!
public override void OnServerDisconnect(NetworkConnection conn)
{ // when the server disconnects someone
// lets grab that player
MyNetworkPlayer player = conn.identity.GetComponent<MyNetworkPlayer>();
players.Remove(player);
base.OnServerDisconnect(conn);
}
public override void OnStopServer()
//what happens when we stop running the server
{
players.Clear();
isGameInProgress = false;
}
public void StartGame()
{ // the game won't start with the less than 2 players
if(players.Count <2){return;}
//if we have more than 2 we can start the game
isGameInProgress = true;
//also we change our scene here
ServerChangeScene("GameScene");
}
public override void OnServerAddPlayer(NetworkConnection conn)
{
base.OnServerAddPlayer(conn);
//reference to network player
MyNetworkPlayer player = conn.identity.GetComponent<MyNetworkPlayer>();
players.Add(player);
// here I assign player's name base on joining order
player.SetDisplayName($"Player {players.Count}");
Debug.Log("Player conn ID: " + conn.connectionId);
//here we dclare who will be a party owner
// if there is only one player he will be a party owner
player.SetPartyOwner(players.Count == 1);
Debug.Log($"There are now {numPlayers} players");
}
//when a client stops, we shold also clear the player's list and currently we only
// add to it on the server
public override void OnStopClient()
{
players.Clear();
}
#endregion
#region Client
[System.Obsolete]
public override void OnClientConnect(NetworkConnection conn)
{
base.OnClientConnect(conn);
Debug.Log("Client connected: " + conn.connectionId);
ClientOnConnected?.Invoke();
}
[System.Obsolete]
public override void OnClientDisconnect(NetworkConnection conn)
{
base.OnClientDisconnect(conn);
ClientOnDisconnected?.Invoke();
}
#endregion
}
And MyNetworkPlayer:
public class MyNetworkPlayer : NetworkBehaviour
{
//Here i refer to a player text and change it based on current player number.
[SerializeField] private TMP_Text displayNameText= null;
[SyncVar(hook = nameof(AuthorityHandlePartyOwnerStateUpdated))]
private bool isPartyOwner = false;
// syncvar that will allow other players to store our name
[SyncVar(hook = nameof(ClientHandleDisplayNameUpdated))]
public string displayName;
// this below is the event created in order to handle the changes of client info
// like it's colour and so on
public static event Action ClientOnInfoUpdated;
//public string which returns the display name
public string GetDisplayName()
{
return displayName;
}
public bool GetIsPartyOwner()
{
return isPartyOwner;
}
public static event Action<bool> AuthorityOnPartyOwnerStateUpdated;
#region Serwer
public override void OnStartServer()
{
DontDestroyOnLoad(gameObject);
}
[Server]
// checking who the party owner is
public void SetPartyOwner(bool state)
{
isPartyOwner = state;
}
[Server]
public void SetDisplayName(string newDisplayName)
{
displayName = newDisplayName;
}
#endregion
#region Commands
[Command]
private void CmdSetDisplayName(string newDisplayName)
{
//here I add the codition saying that the player length can not be shorter than 2 characters
//if(newDisplayName.Length <2 || newDisplayName.Length > 20){ return; }
RpcLogNewName(newDisplayName);
SetDisplayName(newDisplayName);
}
[Command]
// A command so client can tell the server that he wants to start the game
public void CmdStartGame()
{
if(!isPartyOwner) { return; }
//otherwise tell the NetworkManager to stop the game
((MyNetworkManager)NetworkManager.singleton).StartGame();
}
#endregion
#region Authorities
private void AuthorityHandlePartyOwnerStateUpdated(bool oldState, bool newState)
{
// it allows you to change who the party owner is during the lobby
// its goinna be a button to change the authority of the ownershit
if(!hasAuthority) {return; }
// this down below is for the UI button change its statement
AuthorityOnPartyOwnerStateUpdated?.Invoke(newState);
}
#endregion
#region Client
// Here we change the player's name
private void HandleDisplayNameUpdated(string oldName, string newName)
{
displayNameText.text = newName;
}
// Here we connect the reference from MyNetworkManager
public override void OnStartClient()
{
if (NetworkServer.active) {return;}
// whenever a client starts and we're not the host, if we just end up a client, we can
// be added to our list of the players
((MyNetworkManager)NetworkManager.singleton).players.Add(this);
// this below prevcents our player objects from being destroyed when
// changing the scanes
DontDestroyOnLoad(gameObject);
}
private void ClientHandleDisplayNameUpdated(string oldDisplayName, string newDisplayName)
{ // we need to display people's name when they got updated (in the UI)
ClientOnInfoUpdated?.Invoke();
}
public override void OnStopClient()
{
ClientOnInfoUpdated?.Invoke();
//if we are not the server we do this for everyone:
if (!isClientOnly) {return; }
// Here we remove a player from the list of players
((MyNetworkManager)NetworkManager.singleton).players.Remove(this);
//if we are the server we do this for everyone:
if (!hasAuthority) { return; }
}
[ClientRpc]
//rpc log new name is one of our new names being set
private void RpcLogNewName(string newDisplayName)
{
Debug.Log(newDisplayName);
}
#endregion
}
My problem: As I said in possible solution, I don't know how to send connectionId to host and then, based on that, assign corresponding UI element for each different player. I would be really thankful for any advise on how to code that.
After recovering my data with Firebase using a callback in GetValueAsync().ContinueWith(task..) , I would like to instantiate my prefab in order to see the list of scores for my leaderboard. But, it does nothing and I have no errors. The code simply stops in the callback UseSores as soon as it come across on a 'this' or a 'instantiate'.
public class Leaderboardmanager : MonoBehaviour
{
public GameObject rowLeardBoard;
FirebaseDB_Read read;
float positionX;
int nbRows = 10;
void Start()
{
read = (gameObject.AddComponent<FirebaseDB_Read>());
GetScorePlayer();
}
void GetScorePlayer()
{
read.GetScores(UseScores, "entries/LeaderBoard/", nbRows);
}
void UseScores(IList<FirebaseDB_Read.Score> scores)
{
Debug.Log("arrive here");
positionX = this.transform.position.y;
Debug.Log("does not arrive here");
}
}
Here is to get my data :
public class FirebaseDB_Read : MonoBehaviour
{
public class Score
{
public string UID;
public string score;
public int rank;
}
public void GetScores(Action<IList<Score>> callback, string URL_TO_SCORES, int limit)
{
DatabaseReference scoresRef = FirebaseDatabase.DefaultInstance.GetReference(URL_TO_SCORES);
scoresRef.OrderByChild("score").LimitToLast(limit).GetValueAsync().ContinueWith(task =>
{
DataSnapshot snapshot = task.Result;
IList<Score> objectsList = new List<Score> { };
int i = 1;
foreach (var childSnapshot in snapshot.Children)
{
Score score = new Score();
score.rank = i;
score.UID = childSnapshot.Child("UID").GetValue(true).ToString();
score.score = childSnapshot.Child("score").GetValue(true).ToString();
objectsList.Add(score);
i++;
}
callback(objectsList);
});
}
}
This is an often asked problem in Unity: Because you ContinueWith on a background thread!
Unity isn't thread-safe, meaning that most of the Unity API can only be used within the Unity main thread.
Firebase offers an extension specifically for Unity: ContinueWithOnMainThread which assures that the result is handled in the Unity main thread where accessing the API is valid.
scoresRef.OrderByChild("score").LimitToLast(limit).GetValueAsync().ContinueWithOnMainThread(task =>
{
...
});
As alternative you can use kind of a so called "main thread dispatcher" pattern and make sure that the callback is executed in the main thread on the receiver side. The advantage of this would be that the still expensive operations on your list are all executed on a background thread, not affecting the UI performance
scoresRef.OrderByChild("score").LimitToLast(limit).GetValueAsync().ContinueWith(task =>
{
...
});
but then on receiver side in FirebaseDB_Read
private readonly ConcurrentQueue<Action> _mainThreadActions = new ConcurrentQueue<Action>();
private void Update()
{
if(_mainThreadAction.Count > 0)
{
while(_mainThreadActions.TryDequeue(out var action))
{
action?.Invoke();
}
}
}
void GetScorePlayer()
{
read.GetScores(UseScores, "entries/LeaderBoard/", nbRows);
}
void UseScores(IList<FirebaseDB_Read.Score> scores)
{
// handle this in the next main thread update
_mainThreadActions.Enqueue(() =>
{
Debug.Log("arrive here");
positionX = this.transform.position.y;
Debug.Log("does not arrive here");
}
}
which on the offside of course introduces a little overhead for checking for any new actions in Update of course. So if you plan do use multiple of such background actions make sure to implement them in one central place in order to keep the overhead limited ;)
Trying to create a ready check using PUN2 so that all players will load into the game scene at the same time, but I do not understand how to check another players custom property and keep a count of how many players are currently ready and if all are ready then start the game. I think I have a custom property set up for every player that should be but I am unsure if it working at all.
public class HeroSelectController : MonoBehaviour
{
[HideInInspector]
public string selectedHero;
private PhotonView PV;
private bool PlayerReady = false;
private ExitGames.Client.Photon.Hashtable _playerCustomProperties = new ExitGames.Client.Photon.Hashtable();
private void Update()
{
Debug.Log("Player Ready = " + _playerCustomProperties["PlayerReady"]);
}
private void HeroSelect()
{
PlayerReady = true;
selectedHero = "PlayerTest";
PhotonNetwork.SetPlayerCustomProperties(_playerCustomProperties);
_playerCustomProperties["PlayerReady"] = PlayerReady;
}
public void OnClickHeroButton()
{
HeroSelect();
if (PhotonNetwork.IsMasterClient)
{
foreach (var photonPlayer in PhotonNetwork.PlayerList)
{
photonPlayer.CustomProperties["PlayerReady"] = true;
PhotonNetwork.LoadLevel(3);
}
}
}
}
What is currently happening is that the master client can start the game regardless of everyone else's state. Feel like I might be overthinking all of this and there is a much similar solution as I would expect a function like this to be common place as I would expect something similar to be used in many online games so if I am going about completely the wrong way please point me a more suitable direction
Don't know exactly how Photon works but I assume these properties are already synchronized and as far as I understand you simply want some kind of a check like e.g.
private bool AllPlayersReady
{
get
{
foreach (var photonPlayer in PhotonNetwork.PlayerList)
{
if(photonPlayer.CustomProperties["PlayerReady"] == false) return false;
}
return true;
}
}
Which you could probably also shorten using Linq like
using System.Linq;
...
private bool AllPlayersReady => PhotonNetwork.PlayerList.All(player => player.CustomProperties["PlayerReady"] == true);
And then use it like
public void OnClickHeroButton()
{
HeroSelect();
if (!PhotonNetwork.IsMasterClient) return;
if(!AllPlayersReady)
{
return;
}
PhotonNetwork.LoadLevel(3);
}
This can still be enhanced since now the master has to press repeatedly until maybe everyone is ready. I would additionally use a Coroutine like
public void OnClickHeroButton()
{
HeroSelect();
if (!PhotonNetwork.IsMasterClient) return;
StartCoroutine (WaitAllReady ());
}
private IEnumerator WaitAllReady()
{
yield return new WaitUntil (() => AllPlayersReady);
PhotonNetwork.LoadLevel(3);
}
Thanks to Iggy:
Instead of a routine which checks every frame you can use OnPlayerPropertiesUpdate in order to check for the ready state
void OnPlayerPropertiesUpdate(Player targetPlayer, Hashtable changedProps)
{
if (!PhotonNetwork.IsMasterClient) return;
// you can even limit the check to make it
// only if "PlayerReady" is among the changed properties
if(!changedProps.Contains("PlayerReady")) return;
if(!AllPlayersReady) return;
PhotonNetwork.LoadLevel(3);
}
Note: Typed on smartphone but I hope the idea gets clear
I've been working on a simulator for FFXIV;
I keep getting really close to finishing, then I get walled into an object access issue that doesn't let me access the stats of the main player. My project can be found at: https://github.com/eein/chocobro
There's plenty of things i can optimize, I know. :P
Basically i'm kind of brute-force winging it to have objects gain access to the objects they need, but i'm looking for the right way to do things.
I'm going to start rewriting it so don't clutch too hard to the example code but its there to see the issue i'm having. :(
Ideally in the next attempt, this is what i'd like to do:
Start with a player class that contains all of the informatin about the player object (in the future, i'd like to create multiples for full group simulation).
Something like:
int main(){
Player p = new Player();
public void setJob()
{
if (job == "bard"){ Player p = new Bard(); }
if (job == "warrior"){ Player p = new Warrior(); }
}
public class Player
{
private string name {get;set;}
private string job {get;set;}
private string STR;
private string DEX;
private string VIT;
//etc..
public virtual void rotation()
{
}
}
//I want to make the program a bit modular for jobs (roles/classes)
//So..
public class Bard : Player
{
public override void rotation()
{
heavyshot.execute();
//etc.
}
Ability heavyshot = new Heavyshot();
public class Heavyshot : Ability
{
public Heavyshot()
{
name = "Heavy Shot";
potency = 150;
dotPotency = 0;
recastTime = 2.5;
TPcost = 60;
animationDelay = 0.8;
abilityType = "Weaponskill";
castTime = 0.0;
duration = 0.0;
}
public override void impact()
{
//add heavier shot buff activation here
base.impact();
}
}
}
public class Ability{
public int cooldown;
public int cost;
public virtual void impact()
{
public virtual void impact()
{
//Deal some damage.
// !! - the key problem is here, i want to initiate a roll to compare to the players CRIT rating versus the roll to determine the bonus damage. But I can't access the initiated players crit from here. The rating may change depending on abilities used so I can't create a new object. I know i need an object reference but I can't figure it out...
log(time.ToString("F2") + " - " + name +
" Deals " + potency +
" Potency Damage. Next ability at: " + nextability);
}
}
I'm probably not being too clear, but basically I want to be able to access the player's crit from ability, and i'm assuming ability can't be set up this way in order for it to work. Does anyone have a good idea what kind of design pattern I should be using so that the virtual functions in ability can access the parent classes players stats?
Ideally, I want the bard class to contain all of the abilities and stat updates pertaining to the bard job, once bard inherits player and the object is changed to reference the Bard object, how do I make it so abilities created by the Ability class dont need an object reference to the parent at the time of creation when accessing that function.
I'm confusing myself, but many thanks to whoever understands my gibberish and can help!
One option is to pass the crit to the Abillity's constructor.
Another option, if an Ability is always connected to the player.
Have the crit property public with private set:
public class Player
{
private double crit { get; private set;}
...
}
Then pass the Player to the Ability's constructor and hold it there.
Note however that this will increase the coupling.
This could be done like this:
public class Ability
{
protected Player _player;
public Ability(Player player)
{
_player = player;
}
}
you would want to change the Headshot class deriving from Ability as well
public class Headshot : Ability
{
public Headshot(Player player) : base(player)
{
...
}
}
Instead of executing abilities rotation directly, save the abilities in a list. This way, you can have setup method in Player, that injects the player into all abilities in rotation. Adding conditions and some kind of "abilityused", that negates next ability is actually one more reason to express the rotation in some kind of list. So you don't have to duplicate the "abilityused" check everywhere, but have it in one place. Something like this:
public class Player
{
private string name {get;set;}
private string job {get;set;}
private string STR;
private string DEX;
private string VIT;
//etc..
public struct RotationAbility
{
public RotationAbility(Func<bool> cond, Ability ability)
{
this.cond = cond;
this.ability = ability;
}
public Func<bool> cond;
public Ability ability;
}
private List<RotationAbility> rotation = new List<RotationAbility>();
public void execute()
{
foreach (var ab in rotation)
{
if (ab.cond())
ab.ability.execute();
}
}
public void setUpRotation()
{
setUpRotation(rotation);
foreach (var ab in rotation)
{
ab.ability.Player = this;
}
}
protected virtual void setUpRotation(List<RotationAbility> rotation) { }
}
//I want to make the program a bit modular for jobs (roles/classes)
//So..
public class Bard : Player
{
protected override void setUpRotation(List<RotationAbility> rotation)
{
rotation.Add(new RotationAbility(()=>buff>0, new Heavyshot());
//etc.
}
public class Heavyshot : Ability
{
public Heavyshot()
{
name = "Heavy Shot";
potency = 150;
dotPotency = 0;
recastTime = 2.5;
TPcost = 60;
animationDelay = 0.8;
abilityType = "Weaponskill";
castTime = 0.0;
duration = 0.0;
}
public override void impact()
{
//add heavier shot buff activation here
base.impact();
}
}
}
public class Ability{
public Player Player { get; set; }
public int cooldown;
public int cost;
public virtual void impact()
{
//Deal some damage.
// !! - the key problem is here, i want to initiate a roll to compare to the players CRIT rating versus the roll to determine the bonus damage. But I can't access the initiated players crit from here. The rating may change depending on abilities used so I can't create a new object. I know i need an object reference but I can't figure it out...
log(time.ToString("F2") + " - " + name +
" Deals " + potency +
" Potency Damage. Next ability at: " + nextability);
}
}
}
i like to get some thoughts on how to implement a tick based system.
Every action a player or non player got has a initial time to perform and a cooldown time. Once a creatures cooldown time has passed it gets to choose a new action. If the player has to choose a action the game is "paused".
Example:
1: Player heavy swing (50 ticks to perform, 50 ticks to cool down)
2: Game goes on for 50 ticks.
3: NPC's can set actions.
4: Player swings and cools down for 50 ticks.
5: NPC's can set actions.
6: Game paused for the player.
What i currently have works but is not efficient. I have a class with each action as a static method. These method output a struct containing all the data. This will be passed to a actioncue of a individual creature.
Every update loop call the cue and start counting down the attack time if the player has put in a action. Once the attack should be solved i call a static method in the actions class again. And i start counting down the cooldown timer.
So what i should have is probably a list holding all actions and sorting that list skipping unnecessary time/ticks and go straight to the next action. But there will be different types of actions like move, attack, ability and i cant wrap my head around a good implementation of this.
When a creature performs a basic attack this gets called (attack is the creatures own instanced attack struct)
attack = Actions.BasicAttack(this, player, rand);
This is how the Actions class looks like.
public struct Attack
{
public int Damage;
public string Type;
public int Time;
public int Cooldown;
public Creature target;
public bool solved;
}
public static Attack BasicAttack(Creature attacker, Creature defender, Random rand)
{
Attack attack = new Attack();
attack.Damage = rand.Next(attacker.MinBaseDmg, attacker.MaxBaseDmg + 1);
attack.Type = "Melee";
attack.Time = 50;
attack.Cooldown = 30;
attack.target = defender;
attack.solved = false;
return attack;
}
And this gets called in the update method of each creature when the player has a action cued. Tick = 0 if player has no action cued and tick = 1 when player has a action cued up.
protected void ActionCue(int tick)
{
if (attack.target != null)
{
if (attack.Time > 1)
{
Console.WriteLine(attack.Time);
attack.Time -= tick;
this.free = false;
}
else if (!attack.solved)
{
Actions.SolveAttack(attack.Damage, attack.Type, attack.target);
attack.solved = true;
}
else if (attack.solved && attack.Cooldown > 1)
{
//Console.WriteLine(attack.Cooldown);
attack.Cooldown -= tick;
}
else
free = true;
}
}
Consider something like this (i will use pseudocode - its far from being optimized etc. but it might be just fast enough, or set you on your way to optimize what youre trying to do)
class CombatEventList
{
public static AddEvent(CombatEvent event, int ticksTillHappens)
}
virtual class CombatEvent
{
public virtual void CombatAction()
}
class PlayerActionChoice : ComabtEvent
{
public void CombatAction
{
var playerAction = GetUserDecision();//returns i.e CombatEvent PlayerMeeleAttack
CombatEventList.AddEvent(playerAction, 0);
}
}
class PlayerMeeleAttack : CombatEvent
{
int cooldownInTicks = 50;
public void CombatAction
{
MakeAttack()//damages the moster etc - all the stuff the attack is supposed to do
var nextEvent = new PlayerActionChoice();
CombatEventList.AddEvent(nextEvent, cooldownInTicks);
}
}
So, how this works?
We got a list of events.
The list checks all the events that are supposed to happen now and, executes their CombatAction.
In their CombatAction, the events add new events to the list. For example a PlayerMeeleAttack event sets the PlayerActionChoice event after an appropriate cooldown, so that he can take another action later.
After all current CombatEvents are resolved and have added their own CombatEvents to the list, the list checks the next Event (lowest delay)
The list sleeps for the specified number of ticks (the delay of the next Event). Once its done sleeping, it lowers the cooldowns on all events by an appropriate amount, and handles all the current events (those that just hit 0 delay)
This goes in a loop
The list starts with the CombatStartEvent on it, thats going to happen right away(delay 0). It sets the PlayerActionChoice and MonsterActionChoice events in the CombatAction method.
Of course this is far from being optimal, its just a sketch, or an idea for you to think through. There may be better ideas, i didnt give the problem very much thought - but this is obviously more efficient than your current solution :)
Ok after a couple of hours it seems i got this working. Here is the code for anyone who needs it. I am open for feedback too.
This is the ability class, everything needed can be added here. For the tick based system just the time variables are important.
public string AbilityName { get; private set; }
public int minDamage { get; private set; }
public int maxDamage { get; private set; }
public int ActivationTime { get; private set; }
public int CooldownTime { get; private set; }
public int Timer;
public Ability(string AbilityName)
{
if (AbilityName == "attack")
{
this.AbilityName = AbilityName;
minDamage = 10;
maxDamage = 20;
ActivationTime = 20;
CooldownTime = 30;
Timer = ActivationTime;
iconPath = "ability/icon/attack";
}
}
This is the task class, the ability, attacker and targets get passed as parameters, a ability name or type can be used to perform different kinds/types of abilities such as movement vs attacking.
public Ability ability { get; private set; }
public bool onCooldown;
public Creature attacker { get; private set; }
List<Creature> targets = new List<Creature>();
/// <summary>
/// Initiates a attack task
/// </summary>
/// <param name="attacker"></param>
/// <param name="defender"></param>
public Task(Creature attacker, List<Creature> targets, Ability ability)
{
this.ability = ability;
this.attacker = attacker;
this.targets = targets;
onCooldown = false;
}
public void Perform()
{
//performce abilty
Console.WriteLine(attacker.Name + " performce ability");
}
The player or AI can now create a task from the abilities they own like so:
targets.Add(player); //This is just a basic attack so only one "creature" gets in the list
task = new Task(this, targets, abilityList[0]); //Task is created
taskList.Add(task); //Task is added to a list i manage in a main class
free = false; //creature is put on hold and cant do anything till task is completed
This is where most of the magic happens. In the main class this method is being called each update if the player is not "free". I update all the tasks before i do anything with the task next in line because i dont want to edit its stats after updating its status.
private void TaskHandler()
{
int ticksToAdvance = 0;
// get the next task requiring a action
taskList.Sort((x, y) => x.ability.Timer.CompareTo(y.ability.Timer));
//get the amount of cooldown left
ticksToAdvance = taskList[0].ability.Timer;
//Update all tasks
foreach (Task t in taskList)
{
t.ability.Timer -= ticksToAdvance;
}
//check if this task is on cooldown
if (taskList[0].onCooldown)
{
//Reset ability timer, free creature and remove task from the list.
taskList[0].ability.Timer = taskList[0].ability.ActivationTime;
taskList[0].attacker.free = true;
taskList.RemoveAt(0);
}
else
{
//perform ability
taskList[0].Perform();
//set timer to cooldown
taskList[0].onCooldown = true;
taskList[0].ability.Timer = taskList[0].ability.CooldownTime;
}
}