using System.Collections; using System.Collections.Generic; using UnityEngine; using System.Linq; using System.Reflection; using ExitGames.Client.Photon; using ExitGames.Client.Photon.LoadBalancing; using Hashtable = ExitGames.Client.Photon.Hashtable; using Type = System.Type; using Action = System.Action; namespace EntityNetwork { public static class EntityManager { public static Dictionary entities = new Dictionary(); /// /// Get an unused EntityID for a given PlayerID. /// This ID is ensured to be unique for the client, assuming each client has a different PlayerID it will not collide. /// Each playerID's ID space is about two hundred and sixty eight million IDs. /// /// An unused EntityBase ID number /// The player number, ranged [0,127] public static int GetUnusedID(int playerID) { if (playerID > 127) throw new System.ArgumentOutOfRangeException("playerID cannot exceed 127"); if (playerID < 0) throw new System.ArgumentOutOfRangeException("playerID cannot be less than zero"); // Fill all but the topmost byte randomly, then the topmost byte will be an sbyte for player id int player = playerID << 28; int randomInt = Random.Range(0, 0x0FFFFFFF); int proposedID = player | randomInt; // Recursively dig for new ID's on collision while (entities.ContainsKey(proposedID)) { proposedID = GetUnusedID(playerID); } return proposedID; } /// /// Get a reference to an entity of a given ID. /// There is a chance that this entity may have been deleted. /// /// Entity ID public static EntityBase Entity(int id) { EntityBase eb; return entities.TryGetValue(id, out eb) ? eb : null; } /// /// Get a reference to an entity of a given ID. /// There is a chance that this entity may have been deleted. /// /// /// /// public static T Entity(int id) where T : EntityBase{ return Entity(id) as T; } private static void CleanEntities() { var toRemove = new List(entities.Count); foreach(var pair in entities) if (!pair.Value) toRemove.Add(pair.Key); foreach (var key in toRemove) entities.Remove(key); } /// /// Register an entity to receive network events/updates. /// This will fail if the EntityID is already in use. /// /// Registering an entity validates all existing entities and will unsubcribe dead entities. public static void Register(EntityBase eb) { CleanEntities(); //Debug.LogFormat("Registered Entity {0} : {1}",eb.name,eb.EntityID); //Debug.LogFormat("{0} -> {1}", eb.name, string.Join(", ", eb.GetType().GetInterfaces().Select(t => t.Name).ToArray())); if (eb is IAutoSerialize) { eb.StartCoroutine(autoDispatchEntity(eb)); } if (entities.ContainsKey(eb.EntityID)) { var otherEntity = Entity(eb.EntityID); Debug.LogErrorFormat(eb, "{0} has attempted to register over an existing ID {1}, Which belongs to {2}", eb.gameObject.name, eb.EntityID, otherEntity.gameObject.name); throw new System.Exception("Entity ID already in use!"); } entities.Add(eb.EntityID, eb); } /// /// Deregister an EntityBase. This requires enumeraing all entities and is slower than just destroying the EntityBase /// However, in certain cases, like re-registering as a new pooled object or if the object must exist after being removed, it's worth using /// public static void DeRegister(EntityBase eb) { // Grab all keyvaluepairs where the entity is the entity base being deregistered - ToArray is used to collapse the linq to avoid sync issues var toRemove = entities.Select(t => new {id = t.Key, Entity = t.Value}).Where(t => t.Entity == eb).ToArray(); foreach(var removal in toRemove) { entities.Remove(removal.id); } } public static IEnumerator autoDispatchEntity(EntityBase eb) { Debug.LogFormat("Creating Serial Dispatcher for {0} : {1}", eb.name,eb.EntityID); Hashtable h = new Hashtable(); while (true) { h.Clear(); if (eb.isMine) { int code = eb.SerializeAuto(h); if (code != 0) { // If code is 2, message should be reliable // Debug.LogFormat("Dispatching {0}/{2}: {1}", eb.name, h.ToStringFull(), PhotonConstants.EntityUpdateCode); NetworkManager.netMessage(PhotonConstants.EntityUpdateCode, h, code == 2); } } yield return null; } } static EntityManager() { NetworkManager.netHook += OnNet; NetworkManager.onLeave += AllowOrphanSuicidesAndCalls; } // Hook the main events static void OnNet(EventData ev) { if (ev.Code == PhotonConstants.EntityUpdateCode) { var h = (Hashtable)ev[ParameterCode.Data]; // Reject self-aimed events if ((int)ev[ParameterCode.ActorNr] == NetworkManager.localID){ // Show a red particle for an outgoing signal, before rejecting the event var ebs = Entity((int)h[PhotonConstants.eidChar]); NetworkManager.netParticle(ebs, Color.red); return; } var eb = Entity((int)h[PhotonConstants.eidChar]); if (eb) { // Show a blue particle for an incoming singal NetworkManager.netParticle(eb, Color.blue); if (eb is IAutoDeserialize) { eb.DeserializeFull(h); } eb.Deserialize(h); } } if (ev.Code == PhotonConstants.EntityEventCode) { var h = (Hashtable)ev[ParameterCode.Data]; // --- Static Events --- // Param labeled 2 in the hashtable is the EntityBase's ID Type, if a static event call, so if the table contains key 2, run it as a static event object idObject; if (h.TryGetValue(2,out idObject)) { var typeID = (int)idObject; Type entityType; try { entityType = EntityBase.TypeFromID(typeID); } catch { throw new System.Exception("Attempting to call static event on a non-existant type"); } var controlChar = (char)h[0]; object paramObject; if (h.TryGetValue(1,out paramObject)) { EntityBase.InternallyInvokeStatic(entityType, controlChar,(object[])paramObject); } else { EntityBase.InternallyInvokeStatic(entityType, controlChar, null); } return; } // --- Instance Events --- var eb = Entity((int)h[PhotonConstants.eidChar]); if (eb) { var controlChar = (char)h[0]; object paramObject; if (h.TryGetValue(1,out paramObject)) { eb.InternallyInvokeEvent(controlChar,(object[])paramObject); } else { eb.InternallyInvokeEvent(controlChar, null); } } } if (ev.Code == PhotonConstants.EntityInstantiateCode) { var h = (Hashtable)ev[ParameterCode.Data]; DeserializeInstantiate(h); } } /// /// Generate a hashtable describing an object instantiaton for use with DeserializeInstantiate /// Use helper method Instantiate to automatically call and fire this as an event. /// /// public static Hashtable SerializeInstantiate(int authID, Vector3 pos, Quaternion rot, params object[] param) { var H = new Hashtable(); //H.Add('T', typeof(T).ToString()); H.Add('O', authID); H.Add('I', GetUnusedID(authID)); H.Add('T', typeof(T).FullName); H.Add('P', pos); H.Add('R', rot); H.Add('p', param); return H; } /// /// Locally creates the instantiated object described in Hashtable H. /// /// public static void DeserializeInstantiate(Hashtable H) { CheckInstantiators(); //Debug.Log(H.ToStringFull()); var type = typeLookup[H['T'] as string]; var eid = (int)H['I']; var ID = (int)H['O']; var pos = (Vector3)H['P']; var rot = (Quaternion)H['R']; var options = H['p'] as object[]; ActionInstantiate(ID, eid, type, pos, rot, options); //Instantiate(pos, rot, options); } #region Instantiation // Instantiation uses InstanceGameObject / InstanceGameEntity attributes // Actually construct an instantiator object private static void ActionInstantiate(int authID, int entityID, Type T, Vector3 pos, Quaternion rot, object[] param) { MethodInfo mi; if (!InstantiateMethods.TryGetValue(T, out mi)) { throw new System.Exception(string.Format("Type {0} doesn't have an Instantiate Attributed method and isn't Instantiable.", T.Name)); } if (typeof(GameObject).IsAssignableFrom(mi.ReturnType)) { var val = mi.Invoke(null, param) as GameObject; var go = Object.Instantiate(val, pos, rot); // Attempt to set the ID of the entitybase var eb = go.GetComponentInChildren(); if (eb) { eb.EntityID = entityID; eb.authorityID = authID; } go.SendMessage("OnInstantiate", SendMessageOptions.DontRequireReceiver); return; } var rt = mi.ReturnType; if (typeof(EntityBase).IsAssignableFrom(rt)) { var eb = mi.Invoke(null, param) as EntityBase; eb.authorityID = authID; eb.EntityID = entityID; var go = eb.gameObject; var t = eb.transform; if (pos != Vector3.zero) t.position = pos; if (rot != Quaternion.identity) t.rotation = rot; go.SendMessage("OnInstantiate", SendMessageOptions.DontRequireReceiver); return; } throw new System.Exception(string.Format("Type {0}'s Instantiate Method doesn't return an EntityBase or GameObject", T.Name)); } // Helper dictionaries. typeLookup is to help us send types over the wire, InstantiateMethods stores each types instantiator static Dictionary typeLookup; static Dictionary InstantiateMethods; // This is a mess of autodocumentation, mostly due to usage of params and overloads. /// /// Activate type T's EntityBase.Instantation attribute remotely with given parameters, Generating and assigning the appropriate actor ID /// This method returns the HashTable describing the instantation request that can be used to also create the object locally. /// /// public static Hashtable Instantiate(int authID, params object[] param) { return Instantiate(authID, Vector3.zero, Quaternion.identity, param); } /// /// Activate type T's EntityBase.Instantation attribute remotely with given parameters, Generating and assigning the appropriate actor ID /// This method returns the HashTable describing the instantation request that can be used to also create the object locally. /// /// public static Hashtable Instantiate(int authID, Vector3 pos, params object[] param) { return Instantiate(authID, pos, Quaternion.identity, param); } /// /// Activate type T's EntityBase.Instantation attribute remotely with given parameters, Generating and assigning the appropriate actor ID /// This method returns the HashTable describing the instantation request that can be used to also create the object locally. /// /// public static Hashtable Instantiate(int authID, Vector3 pos, Quaternion rot) { return Instantiate(authID, pos, rot, null); } /// /// Activate type T's EntityBase.Instantation attribute remotely with given parameters, Generating and assigning the appropriate actor ID /// This method returns the HashTable describing the instantation request that can be used to also create the object locally. /// /// public static Hashtable Instantiate(int authID, Vector3 pos, Quaternion rot,params object[] param) { var table = SerializeInstantiate(authID, pos, rot, param); if (NetworkManager.isReady) { NetworkManager.net.OpRaiseEvent(PhotonConstants.EntityInstantiateCode, table, true, RaiseEventOptions.Default); } return table; } static bool InstantiatorsBuilt = false; static void CheckInstantiators() { if (InstantiatorsBuilt) return; BuildInstantiators(); InstantiatorsBuilt = true; } // Gather all the instantiaton attributes on entity classes static void BuildInstantiators() { Debug.Log("Buiding Instantiator cache"); InstantiateMethods = new Dictionary(); typeLookup = new Dictionary(); var ebT = typeof(EntityBase); var AllEntityTypes = System.AppDomain.CurrentDomain.GetAssemblies() .SelectMany(s => s.GetTypes()) .Where(t => ebT.IsAssignableFrom(t)); //var AllEntityTypes = Assembly.GetTypes().Where(t => ebT.IsAssignableFrom(t)); foreach(var entityType in AllEntityTypes) { var methods = entityType.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); typeLookup.Add(entityType.FullName, entityType); //Debug.LogFormat("Scanning Type {0}", entityType); foreach(var method in methods) { //Debug.LogFormat("Scanning Method {0}", method.Name); // First look for a GameObject instantiator var ia = method.GetCustomAttributes(typeof(EntityBase.Instantiation),true).FirstOrDefault() as EntityBase.Instantiation; if (ia != null) { InstantiateMethods.Add(entityType, method); Debug.LogFormat("Registering Instantiator {0} for {1} (R: {2})",method.Name,entityType.FullName,method.ReturnType.ToString()); } } } } static void AllowOrphanSuicidesAndCalls(EventData ev) { var toKill = new List(); var players = NetworkManager.net.CurrentRoom.Players.Select(t => t.Value.ID); foreach(var pair in entities) { var e = pair.Value; if (e is IAutoKillOrphans) { if (e.authorityID == -1) continue; if (players.Contains(e.authorityID)) continue; toKill.Add(e.EntityID); } // Send out the orphan callbacks if (e is IOrphanCallback) { if (e.authorityID == -1) return; if (players.Contains(e.authorityID)) continue; (e as IOrphanCallback).OnOrphaned(); } } // Kill the orphans foreach(var killable in toKill) { if (Application.isEditor || Debug.isDebugBuild) { var killEntity = Entity(killable); Debug.LogFormat("Destroying orphaned entity {0} as it's owner {1} has left the room.",killEntity.gameObject.name,killEntity.authorityID); } Object.Destroy(Entity(killable).gameObject); } } #endregion } /// /// Specify that deserialization should be automaticly handled. /// All registered field tokens will be automaticly set using cached setters /// This is not appropriate if you have custom serialization/deserialization logic /// public interface IAutoDeserialize {} /// /// Specify that automatic token handling should be performed on the entity. /// In most cases, this should remove the need to write custom serializers /// This only applies to NetVar's with alwaysSend or updateTime set /// public interface IAutoSerialize {} /// /// Only appropriate for Entities with fixed, pre-determined ID's. /// The entity will attempt to register itself on Awake() /// public interface IAutoRegister {} /// /// Only appropriate for Entities with fixed, pre-determined ID's. /// The entity will absolutely to register itself on Awake() /// public interface IEarlyAutoRegister {} /// /// Assign to an EntityBase so that any time an AuthorityID would be checked we instead check if we're the room master /// Used to clarify network ownership for objects that aren't owned by a player but instead by the room itself /// public interface IMasterOwnsUnclaimed {} /// /// When the authority player disconnects, destroy the entity and attached gameobject that aren't owned by players (EXCEPT with AuthID -1) /// public interface IAutoKillOrphans {} /// /// Adds an OnOrphaned callback - Note this is run whenever a player quits and we are unclaimed without a -1 authority, not just when our authority quits. /// public interface IOrphanCallback { void OnOrphaned(); } }