This repository has been archived on 2025-04-28. You can view files and clone it, but cannot push or open issues or pull requests.
ARPlusSystem/ARPlusSystem-250418/Assets/ARLocation/Experimental/World Voxel Craft/WorldVoxelController.cs

740 lines
24 KiB
C#

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<Voxel> Voxels = new List<Voxel>();
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<WorldChunk> Chunks = new List<WorldChunk>();
}
[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<Voxel>
{
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<ParticleSystemRenderer>().material = hit.Voxel.Instance.GetComponent<MeshRenderer>().material;
i.Play();
state.CurrentChunk.Voxels.Remove(hit.Voxel);
Destroy(hit.Voxel.Instance);
var a = Camera.main.GetComponent<AudioSource>();
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<AudioSource>();
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<BoxCollider>();
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<World>(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<GroundHeight>();
}
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<MeshRenderer>().material.mainTextureScale = new Vector2(50, 50);
// chunk.ChunkPlaneInstance.GetComponent<MeshRenderer>().material.mainTextureScale = new Vector2(chunk.Length, chunk.Length);
// chunk.ChunkPlaneInstance.GetComponent<MeshRenderer>().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<Image>().color = Settings.ButtonSelectedColor;
Elements.GoldBtn.GetComponent<Image>().color = Settings.ButtonNormalColor;
Elements.StoneBtn.GetComponent<Image>().color = Settings.ButtonNormalColor;
Elements.PickAxeBtn.GetComponent<Image>().color = Settings.ButtonNormalColor;
}
void ChooseGold()
{
state.GameState = GameState.Build;
state.CurrentBlock = Blocks.Gold;
Elements.BrickBtn.GetComponent<Image>().color = Settings.ButtonNormalColor;
Elements.GoldBtn.GetComponent<Image>().color = Settings.ButtonSelectedColor;
Elements.StoneBtn.GetComponent<Image>().color = Settings.ButtonNormalColor;
Elements.PickAxeBtn.GetComponent<Image>().color = Settings.ButtonNormalColor;
}
void ChooseStone()
{
state.GameState = GameState.Build;
state.CurrentBlock = Blocks.Stone;
Elements.BrickBtn.GetComponent<Image>().color = Settings.ButtonNormalColor;
Elements.GoldBtn.GetComponent<Image>().color = Settings.ButtonNormalColor;
Elements.StoneBtn.GetComponent<Image>().color = Settings.ButtonSelectedColor;
Elements.PickAxeBtn.GetComponent<Image>().color = Settings.ButtonNormalColor;
}
void ChoosePickaxe()
{
state.GameState = GameState.Destroy;
Elements.BrickBtn.GetComponent<Image>().color = Settings.ButtonNormalColor;
Elements.GoldBtn.GetComponent<Image>().color = Settings.ButtonNormalColor;
Elements.StoneBtn.GetComponent<Image>().color = Settings.ButtonNormalColor;
Elements.PickAxeBtn.GetComponent<Image>().color = Settings.ButtonSelectedColor;
}
void ClearChunk(WorldChunk chunk)
{
if (chunk.ChunkContainer != null)
{
Elements.IndicatorPlane.transform.SetParent(null);
Destroy(chunk.ChunkContainer);
}
chunk.Voxels = new List<Voxel>();
}
void ClearWorld()
{
world.Chunks.ForEach(ClearChunk);
world.Chunks = new List<WorldChunk>();
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 "";
}
}
}
}