using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; namespace ARLocation { public class WorldVoxelController : MonoBehaviour { // Start is called before the first frame update public PrefabDatabase PrefabDatabase; [System.Serializable] class Voxel { public string PrefabId; public int i, j, k; [System.NonSerialized] public GameObject Instance; } struct VoxelHit { public Voxel Voxel; public Vector3 Normal; public Vector3 WorldNorma; } [System.Serializable] class WorldChunk { public List Voxels = new List(); public Location ChunkLocation; public float ChunkRotation; public bool HasLocation; public int Length; [System.NonSerialized] public Vector3 Origin; [System.NonSerialized] public GameObject ChunkContainer; [System.NonSerialized] public Bounds Bounds; //[System.NonSerialized] //public GameObject ChunkPlaneInstance; [System.NonSerialized] public bool IsFresh; } [System.Serializable] class World { public List Chunks = new List(); } [System.Serializable] public class ElementsSettingsData { public Button ClearWorldBtn; public Button PickAxeBtn; public Button BrickBtn; public Button StoneBtn; public Button GoldBtn; public Text DebugText; public AudioClip Create; public AudioClip Destroy; public ParticleSystem BrickParticle; public GameObject IndicatorPlane; public GameObject ChunkPlanePrefab; } public enum Tools { PickAxe, Block } public enum Blocks { Brick, Stone, Gold } [System.Serializable] public class RaycastMarginSettings { public float Top; public float Bottom; public float Left; public float Right; public bool IsInside(Vector2 v) { if (v.x < Left || v.x > (1 - Right)) return false; if (v.y < Bottom || v.y > (1 - Top)) return false; return true; } } [System.Serializable] public class SettingsData { public float CunkScale = 1.0f; public Color ButtonNormalColor; public Color ButtonSelectedColor; } class StateData { public ApplicationState AppState; public GameState GameState; public Blocks CurrentBlock; public WorldChunk CurrentChunk; public Location CurrentLocation; } public ElementsSettingsData Elements; public RaycastMarginSettings RaycastMargins; public SettingsData Settings; private World world = new World(); private readonly StateData state = new StateData(); private void LogText(string str) { Debug.Log(str); Elements.DebugText.text = str; } enum ApplicationState { Initializing, Running }; enum GameState { Destroy, Build }; private IEnumerator StartWorld() { LogText($"Loading previous session file..."); yield return new WaitForSeconds(0.5f); if (RestoreWorldFromLocalStorage()) { LogText($"Restored world with {world.Chunks.Count} chunks"); yield return new WaitForSeconds(0.5f); if (world.Chunks.Count > 0) { double distance; var closestChunk = FindClosestChunk(state.CurrentLocation, out distance, 1000.0); if (closestChunk != null) { LogText($"Found closes chunk at {closestChunk.ChunkLocation}, d = {distance}"); yield return new WaitForSeconds(0.5f); SetCurrentChunk(closestChunk); LogText($"Current Chunk Set"); yield return new WaitForSeconds(0.5f); yield break; } else { LogText($"No chunk nearby!"); yield return new WaitForSeconds(0.5f); var i = 0; foreach (var c in world.Chunks) { var d = Location.HorizontalDistance(state.CurrentLocation, c.ChunkLocation); LogText($"Chunk {i} at {c.ChunkLocation}, d = {d}"); yield return new WaitForSeconds(0.5f); i++; } } } } else { LogText($"No world to restore!"); yield return new WaitForSeconds(0.5f); } LogText("Creating new chunk..."); yield return new WaitForSeconds(0.5f); var chunk = CreateTestChunk(); chunk.IsFresh = true; world.Chunks.Add(chunk); SetCurrentChunk(chunk); UpdateChunkLocation(chunk); LogText("Added new chunk!"); yield return new WaitForSeconds(0.5f); } private IEnumerator Start() { Utils.Misc.HideGameObject(Elements.IndicatorPlane); ARLocationProvider.Instance.OnLocationUpdated.AddListener(OnLocationUpdatedListener); ChooseBrick(); LogText("Starting..."); LogText("Waiting for location..."); yield return new WaitForSeconds(0.5f); yield return StartCoroutine(WaitForLocationServices()); state.CurrentLocation = ARLocationProvider.Instance.CurrentLocation.ToLocation(); LogText($"Location enabled: {state.CurrentLocation}"); yield return new WaitForSeconds(0.5f); yield return StartCoroutine(StartWorld()); LogText($"Starting UI Listeners..."); yield return new WaitForSeconds(0.5f); InitUiListeners(); LogText($"Setting app running..."); yield return new WaitForSeconds(0.5f); state.AppState = ApplicationState.Running; LogText($"App is running!"); } void SetCurrentChunk(WorldChunk chunk) { if ((state.CurrentChunk != null) && (state.CurrentChunk.ChunkContainer != null)) Destroy(state.CurrentChunk.ChunkContainer); state.CurrentChunk = chunk; if (chunk.ChunkContainer == null) { BuildChunk(chunk); } } void AddVoxelToChunk(WorldChunk c, string PrefabId, int i, int j, int k) { Voxel v = new Voxel { PrefabId = PrefabId, i = i, j = j, k = k }; v.Instance = Instantiate(PrefabDatabase.GetEntryById(PrefabId), c.ChunkContainer.transform); v.Instance.transform.localPosition = new Vector3(v.i, v.j, v.k); c.Voxels.Add(v); } WorldChunk CreateDefaultChunk() { return new WorldChunk { Origin = new Vector3(0, -1.4f, 4), Length = 100 }; } WorldChunk CreateTestChunk() { WorldChunk c = new WorldChunk { Voxels = new List { new Voxel { PrefabId = "Brick", Instance = null, i = 0, j = 0, k = 0 } }, Origin = new Vector3(0, -1.4f, 4), Length = 100 }; for (int i = 1; i < 10; i++) { c.Voxels.Add(new Voxel { PrefabId = "Brick", i = i, j = 0, k = 0 }); } for (int i = 1; i < 10; i++) { c.Voxels.Add(new Voxel { PrefabId = "Brick", i = i, j = 0, k = 9 }); } for (int i = 1; i < 10; i++) { c.Voxels.Add(new Voxel { PrefabId = "Brick", i = 0, j = 0, k = i }); } for (int i = 1; i < 10; i++) { c.Voxels.Add(new Voxel { PrefabId = "Brick", i = 9, j = 0, k = i }); } return c; } IEnumerator WaitForLocationServices() { while (!ARLocationProvider.Instance.IsEnabled) { yield return new WaitForSeconds(0.1f); } } bool InputIsDown(out Vector2 pos) { if (Application.isEditor) { pos = Input.mousePosition; return Input.GetMouseButtonDown(0) && RaycastMargins.IsInside(pos / new Vector2(Screen.width, Screen.height)); } if (Input.touchCount == 1 && Input.touches[0].phase == TouchPhase.Began) { pos = Input.touches[0].position; return RaycastMargins.IsInside(pos / new Vector2(Screen.width, Screen.height)); } pos = new Vector3(); return false; } private void Update() { if (state.AppState == ApplicationState.Initializing) return; if (state.CurrentChunk == null) { FindClosestChunkOrCreateNew(state.CurrentLocation); return; } Ray ray = Camera.main.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0)); VoxelHit hit; if (RaycastChunk(ray, state.CurrentChunk, out hit)) { Utils.Misc.ShowGameObject(Elements.IndicatorPlane); var t = Elements.IndicatorPlane.transform; t.SetParent(state.CurrentChunk.ChunkContainer.transform); t.transform.localScale = new Vector3(0.1f, 0.1f, 0.1f); t.transform.localEulerAngles = new Vector3(0, 0, 0); t.transform.localPosition = new Vector3(hit.Voxel.i + Mathf.FloorToInt(hit.Normal.x) * 0.505f, hit.Voxel.j + Mathf.FloorToInt(hit.Normal.y) * 0.505f, hit.Voxel.k + Mathf.FloorToInt(hit.Normal.z) * 0.505f); //LogText("" + hit.Normal); var Normal = hit.Normal; if (Mathf.Abs(Normal.x) < 0.0001f && Mathf.Abs(Normal.y) < 0.0001f) { float sign = Normal.z < 0 ? -1.0f : 1.0f; Elements.IndicatorPlane.transform.localEulerAngles = new Vector3(sign * 90.0f, 0, 0); } else if (Mathf.Abs(Normal.z) < 0.0001f && Mathf.Abs(Normal.y) < 0.0001f) { float sign = Normal.x < 0 ? 1.0f : -1.0f; Elements.IndicatorPlane.transform.localEulerAngles = new Vector3(0, 0, sign * 90.0f); } } else { Utils.Misc.HideGameObject(Elements.IndicatorPlane); return; } Vector2 touchPos; bool isDown = InputIsDown(out touchPos); switch (state.GameState) { case GameState.Destroy: { if (isDown && hit.Voxel.Instance != null) { var i = Instantiate(Elements.BrickParticle); i.transform.position = hit.Voxel.Instance.transform.position; i.GetComponent().material = hit.Voxel.Instance.GetComponent().material; i.Play(); state.CurrentChunk.Voxels.Remove(hit.Voxel); Destroy(hit.Voxel.Instance); var a = Camera.main.GetComponent(); if (a) { a.clip = Elements.Destroy; a.Play(); } } } break; case GameState.Build: { if (isDown) { AddVoxelToChunk(state.CurrentChunk, GetMeshIdForBlock(state.CurrentBlock), hit.Voxel.i + Mathf.FloorToInt(hit.Normal.x), hit.Voxel.j + Mathf.FloorToInt(hit.Normal.y), hit.Voxel.k + Mathf.FloorToInt(hit.Normal.z)); var a = Camera.main.GetComponent(); if (a) { a.clip = Elements.Create; a.Play(); } } } break; default: break; } } bool RaycastChunk(Ray ray, WorldChunk chunk, out VoxelHit hit) { //if (!chunk.Bounds.IntersectRay(ray)) { // hit = new VoxelHit(); // return false; //} VoxelHit currentHit = new VoxelHit(); float currentDistance = 0; bool hasHit = false; foreach (var v in chunk.Voxels) { if (v.Instance) { var collider = v.Instance.GetComponent(); Debug.Assert(collider); RaycastHit h; if (collider.Raycast(ray, out h, chunk.Length)) { if (!hasHit || h.distance < currentDistance) { hasHit = true; currentDistance = h.distance; currentHit = new VoxelHit { Voxel = v, WorldNorma = h.normal, Normal = chunk.ChunkContainer.transform.InverseTransformDirection(h.normal) }; } } } } hit = currentHit; if (hasHit) { hit = currentHit; return true; } else { var chunkOrigin = chunk.ChunkContainer.transform.position; var plane = new Plane(new Vector3(0, 1, 0), -chunkOrigin.y + 0.5f * Settings.CunkScale); float d; if (plane.Raycast(ray, out d)) { var p = chunk.ChunkContainer.transform.InverseTransformPoint(ray.GetPoint(d)); var i = Mathf.FloorToInt(p.x + 0.5f); var j = -1; var k = Mathf.FloorToInt(p.z + 0.5f); hit = new VoxelHit { Voxel = new Voxel { PrefabId = "", i = i, j = j, k = k }, Normal = new Vector3(0, 1, 0) }; return true; } } return false; } bool RestoreWorldFromLocalStorage() { string json = ""; try { json = System.IO.File.ReadAllText(GetJsonFilename(), System.Text.Encoding.UTF8); } catch { Debug.Log("[ARLocation::WorldBuilder::RestoreWorld]: Failed to open json file for reading."); //RandomPopulateWorld(); return false; } world = JsonUtility.FromJson(json); Debug.Log($"Restored world from json file '{GetJsonFilename()}'"); return true; } string GetJsonFilename() { var s = "WorldVoxelCraft"; return Application.persistentDataPath + "/" + s + ".json"; } WorldChunk FindClosestChunk(Location l, out double distance, double maxDistance) { WorldChunk current = null; double currentDistance = 0; foreach (var c in world.Chunks) { var d = Location.HorizontalDistance(c.ChunkLocation, l); if (current == null || d < currentDistance) { current = c; currentDistance = d; } } distance = currentDistance; if (currentDistance > maxDistance) return null; return current; } void BuildChunk(WorldChunk chunk) { chunk.ChunkContainer = new GameObject(); chunk.ChunkContainer.transform.localScale = new Vector3(Settings.CunkScale, Settings.CunkScale, Settings.CunkScale); if (chunk.HasLocation) { PlaceAtLocation.AddPlaceAtComponent(chunk.ChunkContainer, chunk.ChunkLocation, new PlaceAtLocation.PlaceAtOptions { MaxNumberOfLocationUpdates = 2 }); chunk.ChunkContainer.transform.localEulerAngles = new Vector3(0, chunk.ChunkRotation, 0); } else { chunk.ChunkContainer.transform.position = chunk.Origin; chunk.Bounds = new Bounds(chunk.Origin, new Vector3(chunk.Length, chunk.Length, chunk.Length)); chunk.ChunkContainer.AddComponent(); } foreach (var v in chunk.Voxels) { v.Instance = Instantiate(PrefabDatabase.GetEntryById(v.PrefabId), chunk.ChunkContainer.transform); v.Instance.transform.localPosition = new Vector3(v.i, v.j, v.k); } //if (Elements.ChunkPlanePrefab != null) //{ // chunk.ChunkPlaneInstance = Instantiate(Elements.ChunkPlanePrefab, chunk.ChunkContainer.transform); // chunk.ChunkPlaneInstance.transform.localPosition = new Vector3(0, -0.5f, 0); // //chunk.ChunkPlaneInstance.transform.localScale = new Vector3(5, 1, 5); // chunk.ChunkPlaneInstance.transform.localScale = new Vector3(chunk.Length/10, 1, chunk.Length/10); // //chunk.ChunkPlaneInstance.GetComponent().material.mainTextureScale = new Vector2(50, 50); // chunk.ChunkPlaneInstance.GetComponent().material.mainTextureScale = new Vector2(chunk.Length, chunk.Length); // chunk.ChunkPlaneInstance.GetComponent().material.mainTextureOffset = new Vector2(0.5f, 0.5f); //} } private void OnApplicationPause(bool pause) { if (pause) SaveWorldToLocalStorage(); } private void OnDestroy() { SaveWorldToLocalStorage(); } void SaveWorldToLocalStorage() { var json = JsonUtility.ToJson(world); try { System.IO.File.WriteAllText(GetJsonFilename(), json); } catch { Debug.Log("[ARLocation::WorldBuilder::SaveWorld]: Failed to open json file for writing."); return; } Debug.Log("Saved " + GetJsonFilename()); } private void OnLocationUpdatedListener(Location l) { if (state.AppState != ApplicationState.Running) return; state.CurrentLocation = l; FindClosestChunkOrCreateNew(l); if (state.CurrentChunk != null) { UpdateChunkLocation(state.CurrentChunk); } } private void FindClosestChunkOrCreateNew(Location l) { double distance; var newClosestChunk = FindClosestChunk(l, out distance, 1000.0f); if (newClosestChunk != state.CurrentChunk && newClosestChunk != null) { SetCurrentChunk(newClosestChunk); } if (state.CurrentChunk == null) { SetCurrentChunk(CreateDefaultChunk()); } } void UpdateChunkLocation(WorldChunk c) { if (!c.IsFresh) return; c.ChunkLocation = ARLocationManager.Instance.GetLocationForWorldPosition(c.ChunkContainer.transform.position); c.ChunkLocation.Altitude = 0; c.ChunkLocation.AltitudeMode = AltitudeMode.GroundRelative; var arLocationRoot = ARLocationManager.Instance.gameObject.transform; float angle = Vector3.SignedAngle(c.ChunkContainer.transform.forward, arLocationRoot.forward, new Vector3(0, 1, 0)); c.ChunkRotation = angle; c.HasLocation = true; LogText($"Updated chunk location to {c.ChunkLocation}"); } private void InitUiListeners() { Elements.ClearWorldBtn.onClick.AddListener(() => { ClearWorld(); }); Elements.PickAxeBtn.onClick.AddListener(() => { ChoosePickaxe(); }); Elements.BrickBtn.onClick.AddListener(() => { ChooseBrick(); }); Elements.StoneBtn.onClick.AddListener(() => { ChooseStone(); }); Elements.GoldBtn.onClick.AddListener(() => { ChooseGold(); }); } void ChooseBrick() { state.GameState = GameState.Build; state.CurrentBlock = Blocks.Brick; Elements.BrickBtn.GetComponent().color = Settings.ButtonSelectedColor; Elements.GoldBtn.GetComponent().color = Settings.ButtonNormalColor; Elements.StoneBtn.GetComponent().color = Settings.ButtonNormalColor; Elements.PickAxeBtn.GetComponent().color = Settings.ButtonNormalColor; } void ChooseGold() { state.GameState = GameState.Build; state.CurrentBlock = Blocks.Gold; Elements.BrickBtn.GetComponent().color = Settings.ButtonNormalColor; Elements.GoldBtn.GetComponent().color = Settings.ButtonSelectedColor; Elements.StoneBtn.GetComponent().color = Settings.ButtonNormalColor; Elements.PickAxeBtn.GetComponent().color = Settings.ButtonNormalColor; } void ChooseStone() { state.GameState = GameState.Build; state.CurrentBlock = Blocks.Stone; Elements.BrickBtn.GetComponent().color = Settings.ButtonNormalColor; Elements.GoldBtn.GetComponent().color = Settings.ButtonNormalColor; Elements.StoneBtn.GetComponent().color = Settings.ButtonSelectedColor; Elements.PickAxeBtn.GetComponent().color = Settings.ButtonNormalColor; } void ChoosePickaxe() { state.GameState = GameState.Destroy; Elements.BrickBtn.GetComponent().color = Settings.ButtonNormalColor; Elements.GoldBtn.GetComponent().color = Settings.ButtonNormalColor; Elements.StoneBtn.GetComponent().color = Settings.ButtonNormalColor; Elements.PickAxeBtn.GetComponent().color = Settings.ButtonSelectedColor; } void ClearChunk(WorldChunk chunk) { if (chunk.ChunkContainer != null) { Elements.IndicatorPlane.transform.SetParent(null); Destroy(chunk.ChunkContainer); } chunk.Voxels = new List(); } void ClearWorld() { world.Chunks.ForEach(ClearChunk); world.Chunks = new List(); state.CurrentChunk = null; LogText("World cleared!"); } private string GetMeshIdForBlock(Blocks b) { switch (b) { case Blocks.Brick: return "Brick"; case Blocks.Stone: return "Stone"; case Blocks.Gold: return "Gold"; default: return ""; } } } }