using Sirenix.OdinInspector; using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; namespace Beyond { public class ObjectGrid : MonoBehaviour, IJSONSerializable where T : ISpawnable, IGuidable, IPositionable where G : MonoBehaviour { #region ToOverload protected virtual bool UseFrustum { get => false; } protected virtual bool DespawnOnStart { get => false; } #endregion ISpawnable m_spawnable; protected struct GridPos { public GridPos(int x, int z) { this.x = x; this.z = z; } public int x, z; } [Serializable] public class Cell { public Cell(bool wasActive) { guidToItem = new Dictionary(); this.wasActive = wasActive; } public Dictionary guidToItem; public bool wasActive; } [SerializeField] private Camera m_camera; // public class Cell : Dictionary { } protected Cell[,] m_ItemGrid; protected Dictionary m_guidToCell; protected Dictionary m_guidToItem; // Start is called before the first frame update [BoxGroup("Grid Size")] [SerializeField] private bool m_useManualSize = true; [BoxGroup("Grid Size")] [SerializeField] protected Vector3 m_minPos = new Vector3(-1024, 0, -1536), m_maxPos = new Vector3(1024,0,1536); private Bounds m_cellBounds; private int m_lastFrameUpdate = -1; public enum CellSize {_8x8 = 8, _16x16 = 16, _32x32 = 32, _64x64 } [SerializeField] protected CellSize Size = CellSize._32x32; protected float m_size; protected float m_hsize; public float m_visibilityRange = 30f; public float m_checkTimeDelta = 0.1f; public bool m_visualize = false; private float m_timer = 0f; //public Camera m_camera; protected Vector3 m_playerPos; //cached player position protected int m_xCount, m_zCount; private bool m_firstFrame = true; private int m_loopRange; //private Queue m_SpawnQueue; private Vector3 TerrainCenter(Terrain t) { return t.transform.position + t.terrainData.size * 0.5f; } private Vector3 CellPosition(int x, int z) { Vector3 pos; pos.y = 0f; pos.x = ((float)x+.5f) * m_size + m_minPos.x; pos.z = ((float)z+.5f) * m_size + m_minPos.z; return pos; } [Button] protected virtual void PrepareGrid() { m_size = (float) Size; m_loopRange = (int) ((m_visibilityRange / m_size) + 0.5f); if (!m_useManualSize) { m_minPos.x = float.MaxValue; m_maxPos.x = -float.MaxValue; m_minPos.z = float.MaxValue; m_maxPos.z = -float.MaxValue; Terrain[] terrains = GameObject.FindObjectsOfType(); if (terrains.Length > 0) { foreach (var t in terrains) { Vector3 min = t.transform.position; Vector3 max = min + t.terrainData.size; m_minPos.x = m_minPos.x > min.x ? min.x : m_minPos.x; m_maxPos.x = m_maxPos.x < max.x ? max.x : m_maxPos.x; m_minPos.z = m_minPos.z > min.z ? min.z : m_minPos.z; m_maxPos.z = m_maxPos.z < max.z ? max.z : m_maxPos.z; } } else { m_minPos.x = m_minPos.z = -1000f; m_maxPos.x = m_maxPos.z = 1000f; } } m_xCount = (int)((m_maxPos.x - m_minPos.x) / m_size + float.Epsilon) + 1; m_zCount = (int)((m_maxPos.z - m_minPos.z) / m_size + float.Epsilon) + 1; //Debug.Log("xCount: " + xCount + " zCount: " + zCount); m_cellBounds.size = new Vector3(m_size, 200f, m_size); m_ItemGrid = new Cell[m_xCount,m_zCount]; for (int i=0; i(); m_guidToItem = new Dictionary(); } protected static G s_instance; public static G instance { get { return s_instance; } } protected GridPos CellForPosition(Vector3 pos) { GridPos p; p.x = (int)((pos.x - m_minPos.x) / m_size); p.z = (int)((pos.z - m_minPos.z) / m_size); if (p.x < 0 || p.z < 0 || p.x >= m_ItemGrid.GetLength(0) || p.z >= m_ItemGrid.GetLength(1)) { Debug.LogWarning("ItemGrid position out of range: "+pos); p.x = p.z = -1; } return p; } protected bool PositionIsValid(GridPos pos) { if (pos.x < m_xCount && pos.z < m_zCount && pos.x >= 0 && pos.z >= 0) { return true; } return false; } public virtual Guid AddItem(T item) { if (HasItem(item)) { //remove and add again to make sure item is at correct cell RemoveItem(item); } GridPos p = CellForPosition(item.GetPosition()); if (!PositionIsValid(p)) { Debug.LogWarning($"Object is out of range: {item.ToString()}"); return Guid.Empty; } var guid = item.GetGuid(); var cell = m_ItemGrid[p.x, p.z]; if (!cell.guidToItem.ContainsKey(guid)) { cell.guidToItem[guid] = item; if (cell.wasActive != item.IsSpawned()) { if (cell.wasActive) item.Spawn(); else { item.Despawn(); } } m_guidToCell[guid] = cell; m_guidToItem[guid] = item; } return guid; } /// /// Rmoves item from the grid. Returns GUID of the item. /// /// /// public virtual Guid RemoveItem(T item) { /* GridPos p = CellForPosition(item.GetPosition()); if (!PositionIsValid(p)) return Guid.Empty; */ var guid = item.GetGuid(); if (!m_guidToCell.ContainsKey(guid)) return guid; var cell = m_guidToCell[guid]; if (cell != null) { cell.guidToItem.Remove(guid); } m_guidToCell.Remove(guid); m_guidToItem.Remove(guid); return guid; } public bool HasItem(T item) { return m_guidToCell.ContainsKey(item.GetGuid()); } public virtual void UpdateItem(T item) { GridPos p = CellForPosition(item.GetPosition()); if (!PositionIsValid(p)) { Debug.LogError($"Trying to update item outside the grid {item.ToString()}!"); return; } var guid = item.GetGuid(); var cell = m_ItemGrid[p.x, p.z]; if (cell != m_guidToCell[guid]) { RemoveItem(item); AddItem(item); } else { if (cell.wasActive != item.IsSpawned()) { if (cell.wasActive) item.Spawn(); else item.Despawn(); } } } /// /// Return true if given cell is in the range of player /// /// /// /// protected bool CellInRange(int x, int z) { float minX = m_minPos.x + m_size * (float)x; float minZ = m_minPos.z + m_size * (float)z; float maxX = minX + m_size; float maxZ = minZ + m_size; if (maxX < m_playerPos.x - m_visibilityRange) return false; if (minX > m_playerPos.x + m_visibilityRange) return false; if (maxZ < m_playerPos.z - m_visibilityRange) return false; if (minZ > m_playerPos.z + m_visibilityRange) return false; return true; } protected virtual void CheckObjectsInCell(Cell cell, bool activate) { if (activate && !cell.wasActive) { cell.wasActive = true; foreach (var ob in cell.guidToItem) { if (!ob.Value.IsSpawned()) { ob.Value.Spawn(); } } // m_SpawnQueue.Enqueue(cell); } else if (!activate && cell.wasActive) { cell.wasActive = false; //m_ItemGrid. foreach (var ob in cell.guidToItem) { if (ob.Value.IsSpawned()) { ob.Value.Despawn(); } } } } /// /// Checks if objects in cell /// /// /// protected virtual void CheckObjectsInCell(int x, int z, bool activate) { var cell = m_ItemGrid[x, z]; CheckObjectsInCell(cell, activate); } protected int MinXPos(int playerXPos) { int minX = playerXPos - m_loopRange; minX = minX < 0 ? 0 : minX; return minX; } protected int MaxXPos(int playerXPos) { int maxX = playerXPos + m_loopRange; maxX = maxX >= m_xCount ? m_xCount - 1 : maxX; return maxX; } protected int MinZPos(int playerZPos) { int minZ = playerZPos - m_loopRange; minZ = minZ < 0 ? 0 : minZ; return minZ; } protected int MaxZPos(int playerZPos) { int maxZ = playerZPos + m_loopRange; maxZ = maxZ >= m_xCount ? m_xCount - 1 : maxZ; return maxZ; } //public bool IsInRange() protected void DespawnFarObjects() { for (int i = 0; i < m_xCount; i++) { for (int j = 0; j < m_zCount; j++) { if (!CellInRange(i, j)) CheckObjectsInCell(i, j, false); } } } bool CellIsVisible2(int x, int z) { bool visible = false; Vector3 size = Vector3.one * m_size; Vector3 pos = Vector3.zero; pos.x = m_minPos.x + ((float)x + 0.5f) * m_size; pos.z = m_minPos.z + ((float)z + 0.5f) * m_size; Bounds bb = new Bounds(pos, new Vector3(m_size, 10000f, m_size)); var planes = GeometryUtility.CalculateFrustumPlanes(Camera.main); visible = GeometryUtility.TestPlanesAABB(planes, bb); return visible; } public void BoundsOfCell(int x, int z, ref Bounds bounds) { Vector3 pos = bounds.center; pos.x = m_minPos.x + ((float)x + 0.5f) * m_size; pos.z = m_minPos.z + ((float)z + 0.5f) * m_size; bounds.center = pos; } public void CheckAllObjects(bool despawnFarObjects = true) { if (despawnFarObjects) { DespawnFarObjects(); } m_playerPos = Player.Instance.transform.position; GridPos playerGrid = CellForPosition(m_playerPos); int minX = MinXPos(playerGrid.x); int maxX = MaxXPos(playerGrid.x); int minZ = MinZPos(playerGrid.z); int maxZ = MaxZPos(playerGrid.z); if (!UseFrustum) { for (int x = minX; x <= maxX; x++) { for (int z = minZ; z <= maxZ; z++) { CheckObjectsInCell(x, z, true); } } } else { var frustumPlanes = GeometryUtility.CalculateFrustumPlanes(m_camera); for (int x = minX; x <= maxX; x++) { for (int z = minZ; z <= maxZ; z++) { BoundsOfCell(x,z, ref m_cellBounds); var visible = GeometryUtility.TestPlanesAABB(frustumPlanes, m_cellBounds); CheckObjectsInCell(x, z, visible); } } } } protected virtual void CheckCellsThatChangedWithFrustum() { GridPos prevGridPos = CellForPosition(m_playerPos); m_playerPos = Player.Instance.transform.position; GridPos playerGrid = CellForPosition(m_playerPos); //Bounds bb = new Bounds(pos, new Vector3(m_size, 200f, m_size)); var frustumPlanes = GeometryUtility.CalculateFrustumPlanes(m_camera); int minZ = MinZPos(playerGrid.z); int maxZ = MaxZPos(playerGrid.z); int minX = MinXPos(playerGrid.x); int maxX = MaxXPos(playerGrid.x); //check all current cells to check if frustum visibility changed for (int x = minX; x <= maxX; x++) { for (int z = minZ; z <= maxZ; z++) { BoundsOfCell(x,z, ref m_cellBounds); var visible = GeometryUtility.TestPlanesAABB(frustumPlanes, m_cellBounds); CheckObjectsInCell(x, z, visible); } } if (playerGrid.x == prevGridPos.x && prevGridPos.z == playerGrid.z) { //nothing has change, return return; } if (Math.Abs(playerGrid.x - prevGridPos.x) > 1 || Math.Abs(prevGridPos.z - playerGrid.z) > 1) { //larger position change, update all objects. Despawn object that are out of range. CheckAllObjects(true); return; } // CheckAllObjects(true); if (Math.Abs(playerGrid.x - prevGridPos.x) == 1) { int activeX,deactiveX = 0; if (playerGrid.x > prevGridPos.x) { activeX = MaxXPos(playerGrid.x); deactiveX = MinXPos(prevGridPos.x); //deactivate } else { deactiveX = MaxXPos(prevGridPos.x); activeX = MinXPos(playerGrid.x); } if (Math.Abs(deactiveX - playerGrid.x) > m_loopRange) { for (int z = minZ; z <= maxZ; z++) { CheckObjectsInCell(deactiveX, z, false); } } if (Math.Abs(activeX - playerGrid.x) >= m_loopRange) { for (int z = minZ; z <= maxZ; z++) { BoundsOfCell(activeX,z, ref m_cellBounds); var visible = GeometryUtility.TestPlanesAABB(frustumPlanes, m_cellBounds); CheckObjectsInCell(activeX, z, visible); } } } if (playerGrid.z != prevGridPos.z) { int activeZ,deactiveZ = 0; if (playerGrid.z > prevGridPos.z) { activeZ = MaxZPos(playerGrid.z); deactiveZ = MinZPos(prevGridPos.z); //deactivate } else { deactiveZ = MaxZPos(prevGridPos.z); activeZ = MinZPos(playerGrid.z); } if (Math.Abs(deactiveZ - playerGrid.z) > m_loopRange) { for (int x = minX; x <= maxX; x++) { CheckObjectsInCell(x, deactiveZ, false); } } if (Math.Abs(activeZ - playerGrid.z) >= m_loopRange) { for (int x = minX; x <= maxX; x++) { BoundsOfCell(x,activeZ, ref m_cellBounds); var visible = GeometryUtility.TestPlanesAABB(frustumPlanes, m_cellBounds); CheckObjectsInCell(x, activeZ, visible); } } } } bool PlayerCellChanged() { GridPos prevGridPos = CellForPosition(m_playerPos); GridPos playerGrid = CellForPosition(Player.Instance.transform.position); if (playerGrid.x == prevGridPos.x && prevGridPos.z == playerGrid.z) { return false; } return true; } protected virtual void CheckCellsThatChanged() { if (!PlayerCellChanged()) return; GridPos prevGridPos = CellForPosition(m_playerPos); m_playerPos = Player.Instance.transform.position; GridPos playerGrid = CellForPosition(m_playerPos); if (Math.Abs(playerGrid.x - prevGridPos.x) > 1 || Math.Abs(prevGridPos.z - playerGrid.z) > 1) { //larger position change, update all objects. Despawn object that are out of range. CheckAllObjects(true); return; } //CheckAllObjects(true); if (Math.Abs(playerGrid.x - prevGridPos.x) == 1) { int minZ = MinZPos(playerGrid.z); int maxZ = MaxZPos(playerGrid.z); int activeX,deactiveX = 0; if (playerGrid.x > prevGridPos.x) { activeX = MaxXPos(playerGrid.x); deactiveX = MinXPos(prevGridPos.x); //deactivate } else { deactiveX = MaxXPos(prevGridPos.x); activeX = MinXPos(playerGrid.x); } if (Math.Abs(deactiveX - playerGrid.x) > m_loopRange) { for (int z = minZ; z <= maxZ; z++) { CheckObjectsInCell(deactiveX, z, false); } } if (Math.Abs(activeX - playerGrid.x) >= m_loopRange) { for (int z = minZ; z <= maxZ; z++) { CheckObjectsInCell(activeX, z, true); } } } if (playerGrid.z != prevGridPos.z) { int minX = MinXPos(playerGrid.x); int maxX = MaxXPos(playerGrid.x); int activeZ,deactiveZ = 0; if (playerGrid.z > prevGridPos.z) { activeZ = MaxZPos(playerGrid.z); deactiveZ = MinZPos(prevGridPos.z); //deactivate } else { deactiveZ = MaxZPos(prevGridPos.z); activeZ = MinZPos(playerGrid.z); } if (Math.Abs(deactiveZ - playerGrid.z) > m_loopRange) { for (int x = minX; x <= maxX; x++) { CheckObjectsInCell(x, deactiveZ, false); } } if (Math.Abs(activeZ - playerGrid.z) >= m_loopRange) { for (int x = minX; x <= maxX; x++) { CheckObjectsInCell(x, activeZ, true); } } } } protected void CheckCellsToActivate() { if (m_lastFrameUpdate == Time.renderedFrameCount) return; if (m_firstFrame) { CheckAllObjects(DespawnOnStart); m_firstFrame = false; } else { if (UseFrustum) { CheckCellsThatChangedWithFrustum(); } else { CheckCellsThatChanged(); } } m_lastFrameUpdate = Time.renderedFrameCount; } protected void OnDrawGizmosSelected() { if (m_ItemGrid == null || !m_visualize) return; Color c = Color.gray; c.a = 0.7f; Gizmos.color = c; Vector3 size = Vector3.one * m_size; Vector3 pos = Vector3.zero; if (Player.Instance) { m_playerPos = Player.Instance.transform.position; pos.y = m_playerPos.y; } for (int i = 0; i < m_xCount; i++) for (int j = 0; j < m_zCount; j++) { pos.x = m_minPos.x + ((float)i + 0.5f) * m_size; pos.z = m_minPos.z + ((float)j + 0.5f) * m_size; /* if (m_player) { if (CellInRange(i, j)) Gizmos.color = Color.red; else Gizmos.color = c; } */ var cell = m_ItemGrid[i, j]; if (cell.wasActive) { Gizmos.color = Color.red; } else { Gizmos.color = c; } Gizmos.DrawWireCube(pos, size); } } protected virtual void Awake() { if (s_instance == null) { s_instance = this as G; } else { Debug.LogError("ObjectGrid already exists!"); DestroyImmediate(gameObject); } //m_SpawnQueue = new Queue(100); PrepareGrid(); SceneManager.sceneUnloaded += SceneManagerOnsceneUnloaded; } protected void OnDestroy() { SceneManager.sceneUnloaded -= SceneManagerOnsceneUnloaded; } private void SceneManagerOnsceneUnloaded(Scene arg0) { CheckCellsToActivate(); } protected virtual void Start() { CheckCellsToActivate(); } /* private void SpawnCellsInaQueue() { for (int i=0; i m_checkTimeDelta || PlayerCellChanged()) { m_timer = 0f; CheckCellsToActivate(); } //SpawnCellsInaQueue(); } //protected fsSerializer m_Serializer = new fsSerializer(); public virtual string SerializeToJSON() { return ""; } public virtual void DeserializeFromJSON(string js) { } } }