I put this together today and will try it out tomorrow (Saturday here)
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
public class InteriorCheck : MonoBehaviour {
int maxDistanceToCheck = 5; //will turn up after testing time spent and threading
int startingX;
int startingY;
int startingZ;
Structure structure;
Dictionary<int, Dictionary<int, HashSet>> checkedAreas;
bool airtight;
bool threadIsActive = false;
float startTime;
public void CheckInterior (Structure _structure, int x, int y, int z) {
startTime = Time.timeSinceLevelLoad;
startingX = x;
startingY = y;
startingZ = z;
structure = _structure;
checkedAreas = new Dictionary<int, Dictionary<int, HashSet>>();
airtight = true;
bool useThreading = false;
if (useThreading) {
Debug.Log("using threading");
StartCoroutine(CheckInteriorThreaded());
}
else {
Debug.Log("not using threading");
//start from the beginning
CheckArea(x, y, z)
//assign the result
structure.airtight = airtight;
Debug.Log("Time spent checking interior: " + (Time.timeSinceLevelLoad - startTime).ToString());
}
}
IEnumerator CheckInteriorThreaded () {
while (threadIsActive) {
yield return new WaitForSeconds(1f);
Debug.Log("Waiting for thread");
}
threadIsActive = true;
Thread thread = new Thread(CheckInteriorViaThread);
thread.Start();
}
void CheckInteriorViaThread () {
//start from the beginning
CheckArea(x, y, z)
//assign the result
structure.airtight = airtight;
Debug.Log("Time spent checking interior: " + (Time.timeSinceLevelLoad - startTime).ToString());
}
bool CheckArea (int x, int y, int z) {
//don't go more than maxDistanceToCheck from the start. This is the only way airtight gets set to false
if (Mathf.Abs(startingX - x) > maxDistanceToCheck) { airtight = false; }
if (Mathf.Abs(startingY - y) > maxDistanceToCheck) { airtight = false; }
if (Mathf.Abs(startingZ - z) > maxDistanceToCheck) { airtight = false; }
//cancel all further operations if a leak has been found
if (!airtight) { return; }
//don't double check
if (HasAreaBeenChecked(x, y, z)) { return; }
//Add to checked list first to prevent looping back onto this spot
AddCheckedArea(x, y, z);
//if the area doesn't exist, just keep moving
if (!structure.areas.ContainsKey(x) || !structure.areas[x].ContainsKey(y) || !structure.areas[x][y].Contains(z)) {
if (!HasAreaBeenChecked(x + 1, y, z)) { CheckArea(x + 1, y, z); } //N
if (!HasAreaBeenChecked(x, y, z - 1)) { CheckArea(x, y, z - 1); } //E
if (!HasAreaBeenChecked(x - 1, y, z)) { CheckArea(x - 1, y, z); } //S
if (!HasAreaBeenChecked(x, y, z + 1)) { CheckArea(x, y, z + 1); } //W
if (!HasAreaBeenChecked(x, y + 1, z)) { CheckArea(x, y + 1, z); } //U
if (!HasAreaBeenChecked(x, y - 1, z)) { CheckArea(x, y - 1, z); } //D
}
//if the area contains a foundation it is airtight
else if (structure.areas[x][y][z].floor.buildingRecipe == BuildingRecipes.WoodFoundation ||
structure.areas[x][y][z].floor.buildingRecipe == BuildingRecipes.StoneFoundation) {
return;
}
//if the area does exist and is not a foundation test it
else {
//if an airtight building exists don't move in that direction, can still flood from other directions
if (!IsEdgeAirtight(x + 1, y, z, BuildingSlots.SouthEdge)) { CheckArea(x + 1, y, z); } //N
if (!IsEdgeAirtight(x, y, z, BuildingSlots.EastEdge)) { CheckArea(x, y, z); } //E
if (!IsEdgeAirtight(x, y, z, BuildingSlots.SouthEdge)) { CheckArea(x, y, z); } //S
if (!IsEdgeAirtight(x, y, z + 1, BuildingSlots.EastEdge)) { CheckArea(x, y, z + 1); } //W
if (!IsEdgeAirtight(x, y + 1, z, BuildingSlots.Floor)) { CheckArea(x, y + 1, z); } //U
if (!IsEdgeAirtight(x, y, z, BuildingSlots.Floor)) { CheckArea(x, y, z); } //D
}
}
bool HasAreaBeenChecked (int x, int y, int z) {
if (checkedAreas.ContainsKey(x) && checkedAreas[x].ContainsKey(y) && checkedAreas[x][y].Contains(z)) {
return true;
}
return false;
}
void AddCheckedArea (int x, int z) {
if (!checkedAreas.ContainsKey(x)) {
checkedAreas.Add(x, new Dictionary<int, HashSet<int>>());
checkedAreas[x].Add(y, new HashSet<int>());
checkedAreas[x][y].Add(z);
}
else if (!checkedAreas[x].ContainsKey(y)) {
checkedAreas[x].Add(y, new HashSet<int>());
checkedAreas[x][y].Add(z);
}
else if (!checkedAreas[x][y].Contains(z)) {
checkedAreas[x][y].Add(z);
}
else { Debug.LogError("already checked area being added"); }
}
bool AreaExists (int x, int y, int z) {
if (!structure.areas.ContainsKey(x) || !structure.areas[x].ContainsKey(y) || !structure.areas[x][y].Contains(z)) {
return false;
}
return true;
}
bool IsEdgeAirtight (int x, int y, int z, BuildingSlots slot) {
//if the area does not exist the edge cannot be airtight
if (!AreaExists(x, y, z)) {
return false;
}
switch (slot) {
case BuildingSlots.SouthEdge:
if (IsRecipeAirTight(structure.area[x][y][z].southEdge)) { return true; }
else { return false; }
case BuildingSlots.EastEdge:
if (IsRecipeAirTight(structure.area[x][y][z].eastEdge)) { return true; }
else { return false; }
case BuildingSlots.Floor:
if (IsRecipeAirTight(structure.area[x][y][z].floor)) { return true; }
else { return false; }
}
}
bool IsRecipeAirtight (Building building) {
//if it's a wall
if (building.buildingRecipe == BuildingRecipes.WoodWall ||
building.buildingRecipe == BuildingRecipes.StoneWall) {
return true;
}
//if it's a floor
else if (building.buildingRecipe == BuildingRecipes.WoodFloor ||
building.buildingRecipe == BuildingRecipes.StoneFloor) {
return true;
}
//if it's a doorway with a door
else if (building.buildingRecipe == BuildingRecipes.WoodDoorway && building.upgrade == BuildingUpgrades.WoodDoor) {
return true;
}
//wood window
//stone door
//stone window
return false;
}
}
public class Structure {
public Vector3 localPosition; //local start position on the terrain
public float rotation;
public Dictionary<int, Dictionary<int, Dictionary<int, BuildingArea>>> areas; //x,y,z
public bool airtight = false;
}
public class BuildingArea {
public Building eastEdge = null;
public Building southEdge = null; //north, west, and ceiling are south, east, and floor on neighbors
public Building center = null;
public Building floor = null;
}
public class Building {
public BuildingRecipes buildingRecipe = BuildingRecipes.Null; //enum
public BuildingSlot slot = BuildingSlot.Null; //enum
public BuildingUpgrades upgrade = BuildingUpgrades.Null; //enum
}
I’m imagining a standalone structure grid per building started, sort of like Empyrion (awesome game if you haven’t played it) or at least that what it feels like in Empyrion… That way I can run it off thread using only data and have it checking to make sure walls and things are not stairs/things that should not be considered “airtight”.
The problem with this in-game would be limiting the size of a particular building. Off thread though, I might be able to check 50x50x50 or more per frame on most machines, and that would be a lot more than necessary. Ultimately the outer border is the only way to limit the math from going off into space, and the only way that a “leak” can be detected. You should never be able to reach that border, and if you don’t you’re airtight, otherwise in a 3D scenario there is no other way to tell. Consider a spiral, you could run into all four corners over and over, but ultimately the space is not closed.
I think making it clear that each building is separate, with perhaps a name and information panel, and maybe a toggle to show how far the building can be built before it hits it’s max size would be player friendly.
The only problem I still foresee is interaction with the ground, and buildings with multiple rooms. If someone tries to lay foundations across a 100x100 area and then build on top of that, the buildings will basically all become big chunks of rooms attached to one another as far as the Structure system is concerned. Might have to educate players on starting new buildings per building. This could also lead to saving buildings for trading/quick building/copying, again like Empyrion. If the performance is good enough I will remove the size limitation and use the 3d min/maxs of the Structure grid.
Rooms
Still need to come up with something for rooms though… Currently I’m developing this because I want to allow players to summon NPCs like Terraria and Starbound do. When a summoning item is placed the interior check engages. So it doesn’t have to happen on everything, but for warmth/wind tests it would also be useful. When the checks run from a summoning item it can be per room and work fine. If all structures run this check every time they are changed it might be… overboard.