Added WFC and respective parts (Tile, World, Layer, WFC, ResourceLoader)

This commit is contained in:
=
2026-04-19 13:03:56 +02:00
parent 3e823cecbd
commit e6522f2db9
16 changed files with 749 additions and 0 deletions
+38
View File
@@ -0,0 +1,38 @@
[gd_scene format=3 uid="uid://cgsmfi2s51cbd"]
[ext_resource type="Script" uid="uid://br2udyi6t8yvf" path="res://Scripts/World.cs" id="1_xkndl"]
[ext_resource type="Script" uid="uid://dqrdb3bvws6b6" path="res://Scripts/SteamworksHandler.cs" id="2_xkndl"]
[ext_resource type="Script" uid="uid://c7khr6oist3ku" path="res://Scripts/Camera3d.cs" id="3_u44n3"]
[sub_resource type="CompressedTexture2D" id="CompressedTexture2D_u44n3"]
load_path = "res://.godot/imported/Background.png-e8880a50f4091751eaed728281d3c21e.s3tc.ctex"
[sub_resource type="PanoramaSkyMaterial" id="PanoramaSkyMaterial_u44n3"]
panorama = SubResource("CompressedTexture2D_u44n3")
[sub_resource type="Sky" id="Sky_u44n3"]
sky_material = SubResource("PanoramaSkyMaterial_u44n3")
[sub_resource type="Environment" id="Environment_sb48q"]
background_mode = 1
background_color = Color(0.27141052, 0.1874483, 0.13788113, 1)
sky = SubResource("Sky_u44n3")
[node name="Main" type="Node3D" unique_id=234207355]
[node name="World" type="Node3D" parent="." unique_id=770208789]
script = ExtResource("1_xkndl")
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="World" unique_id=1521742758]
transform = Transform3D(1, 0, 0, 0, 0.89061797, 0.45475236, 0, -0.45475236, 0.89061797, 0, 10.721322, 3.5568523)
[node name="SteamworksHandler" type="Node" parent="." unique_id=1183440473]
script = ExtResource("2_xkndl")
[node name="Camera3D" type="Camera3D" parent="." unique_id=161504606]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 30, 20, 30)
current = true
script = ExtResource("3_u44n3")
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=377970686]
environment = SubResource("Environment_sb48q")
+6
View File
@@ -0,0 +1,6 @@
[gd_scene format=3 uid="uid://ck10wk10me3tx"]
[ext_resource type="Script" uid="uid://dkg0vq75koeig" path="res://Scripts/Helpers/Layer.cs" id="1_trar1"]
[node name="Node3D" type="Node3D" unique_id=724642284]
script = ExtResource("1_trar1")
+40
View File
@@ -0,0 +1,40 @@
using Godot;
using static GameData;
public partial class Camera3d : Camera3D
{
[Export] public float Speed = 7.5f;
[Export] public float MouseSensitivity = 0.2f;
[Export] public float ScrollStrength = 5.0f;
private Vector2 _mouseDelta;
public override void _Process(double delta)
{
float d = (float)delta;
var rotation = RotationDegrees;
rotation.X = Mathf.Clamp(rotation.X, -90f, 90f);
RotationDegrees = rotation;
_mouseDelta = Vector2.Zero;
Vector3 direction = Vector3.Zero;
if (Input.IsActionPressed("move_forward")&& Position.Z > 0) direction += Transform.Basis.Z;
if (Input.IsActionPressed("move_backward")&& Position.Z < layerSize * 4) direction -= Transform.Basis.Z;
if (Input.IsActionPressed("move_left") && Position.X > 0) direction -= Transform.Basis.X;
if (Input.IsActionPressed("move_right") && Position.X < layerSize * 4) direction += Transform.Basis.X;
if (direction != Vector3.Zero)
{
direction = direction.Normalized() * (Speed + 3 * Mathf.Log(Position.Y) + 1) * d;
Translate(direction);
}
if (Input.IsActionJustPressed("zoom_in") && Position.Y > 10)
Translate(Transform.Basis.Y * ScrollStrength);
if (Input.IsActionJustPressed("zoom_out") && Position.Y < 50)
Translate(-Transform.Basis.Y * ScrollStrength);
}
}
+1
View File
@@ -0,0 +1 @@
uid://c7khr6oist3ku
+12
View File
@@ -0,0 +1,12 @@
public partial class GameData
{
//Amount of layers generated
public static int ruinSize = 10;
//Width+Height of layers (+1 for the border)
public static int layerSize = 20 + 1;
//Current layer the player wants to see
public static int currentLayer = 0;
//The layer that is currently visible
public static int visibleLayer = 0;
}
+1
View File
@@ -0,0 +1 @@
uid://dgebbx4nkktu6
+231
View File
@@ -0,0 +1,231 @@
using Godot;
using System;
using System.Collections.Generic;
using System.Linq;
using static WFC;
public partial class Layer : Node3D
{
public Tile[,] tiles;
int layerSize;
Tile tile;
int level;
bool updateFailed = false;
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
}
// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
}
public void SetupLayer(int layerSize, int level, Dictionary<string, MeshInstance3D> tileMeshes)
{
this.layerSize = layerSize;
this.level = level;
tiles = new Tile[layerSize, layerSize];
GenerateBaseStructure(tileMeshes);
int safetyCounter = 0;
while (true)
{
if (GenerateLayer())
{
GD.Print("Layer generated");
break;
}
else
{
GD.PrintErr("Layer failed... trying again");
}
ResetLayer(tileMeshes);
safetyCounter++;
if (safetyCounter > 1000) break;
}
}
private void GenerateBaseStructure(Dictionary<string, MeshInstance3D> tileMeshes)
{
Vector3 position;
float offsetX;
float offsetY = level * 4 * -1;
float offsetZ;
for (int x = 0; x < layerSize; x++)
{
offsetX = x * 4;
for (int y = 0; y < layerSize; y++)
{
offsetZ = y * 4;
position = new Vector3(offsetX, offsetY, offsetZ);
tile = new Tile();
tile.SetMeshes(tileMeshes);
tile.Position = position;
tile.GridPosition = new Vector2I(x, y);
tiles[x, y] = tile;
}
}
}
private void ResetLayer(Dictionary<string, MeshInstance3D> tileMeshes)
{
for (int x = 0; x < layerSize; x++)
{
for (int y = 0; y < layerSize; y++)
{
tiles[x, y].Reset(tileMeshes);
}
}
}
private bool GenerateBorder()
{
for (int x = 0; x < layerSize; x++)
{
for (int y = 0; y < layerSize; y++)
{
if (x == 0 || y == 0 || x == layerSize - 1 || y == layerSize - 1)
{
var tile = tiles[x, y];
string result = tile.Collapse("border");
if (result == "ERR")
return false;
NewPropagate(new Vector2I(x, y));
}
}
}
return true;
}
public bool GenerateLayer()
{
bool result = true;
int safetyCounter = 0;
if (!GenerateBorder())
{
return false;
}
Vector2I position = GetSmallestPossibilities();
while (true)
{
string keyword = "";
if (position.X == 0 || position.X == layerSize - 1 || position.Y == 0 || position.Y == layerSize - 1)
{
keyword = tiles[position.X, position.Y].Collapse("border");
}
else
{
keyword = tiles[position.X, position.Y].Collapse("");
}
if (keyword == "ERR")
{
GD.Print("Error in WFC during collapse!");
return false;
}
if (keyword != "")
{
NewPropagate(position);
if (updateFailed) break;
position = GetSmallestPossibilities();
if (position == new Vector2(-100, -100))
{
break;
}
continue;
}
safetyCounter++;
if (safetyCounter == layerSize * layerSize)
{
GD.Print("Error in WFC, overflow!");
return false;
}
}
if (updateFailed) return false;
//Spawn is a tile border, redo world generation
if (tiles[1, 1].collapsedMesh == "border") return false;
//Player has over 80% of tiles available without destroying walls => Results in about 95% success rate
//Not necessarily needed but improves the overall generation percentage at a low resource cost
if (!WFC.IsMapConnected(tiles, 0.8f)) return false;
return result;
}
private void NewPropagate(Vector2I startPos)
{
Queue<Vector2I> queue = new Queue<Vector2I>();
queue.Enqueue(startPos);
while (queue.Count > 0)
{
Vector2I currentPos = queue.Dequeue();
Tile currentTile = tiles[currentPos.X, currentPos.Y];
// Use CURRENT state of tile
var currentPossibilities = currentTile.collapsedMesh != null
? new HashSet<string> { currentTile.collapsedMesh }
: new HashSet<string>(currentTile.tileMeshes.Keys);
for (int i = 0; i < dirs.Length; i++)
{
Vector2I newPos = currentPos + offsets[i];
if (!InBounds(newPos, layerSize, true)) continue;
Tile neighborTile = tiles[newPos.X, newPos.Y];
HashSet<string> allowed = new HashSet<string>();
foreach (string neighborOption in neighborTile.tileMeshes.Keys)
{
foreach (string item in currentPossibilities)
{
if (WFC.CanConnect(item, neighborOption, dirs[i], false))
{
allowed.Add(neighborOption);
break;
}
}
}
int updateCount = neighborTile.Propagate(allowed);
if (updateCount == int.MaxValue)
{
GD.Print("WFC Error! No meshes left");
updateFailed = true;
return;
}
// ONLY enqueue if something changed
if (updateCount > 0)
{
queue.Enqueue(newPos);
}
}
}
}
private Vector2I GetSmallestPossibilities()
{
Vector2I result = new Vector2I(-100, -100);
int lowest = 100;
int current;
for (int x = 0; x < layerSize; x++)
{
for (int y = 0; y < layerSize; y++)
{
if (tiles[x, y].collapsedMesh != null) continue;
current = tiles[x, y].tileMeshes.Count;
if (current < lowest)
{
result = new Vector2I(x, y);
lowest = current;
}
}
}
return result;
}
}
+1
View File
@@ -0,0 +1 @@
uid://dkg0vq75koeig
+25
View File
@@ -0,0 +1,25 @@
using Godot;
using System;
using System.Collections.Generic;
using System.Linq;
public partial class ResourceLoader
{
public static PackedScene LoadLayerPrefab()
{
return GD.Load<PackedScene>($"res://Prefabs/Layer.tscn");
}
public static Dictionary<string, MeshInstance3D> LoadTiles()
{
Dictionary<string, MeshInstance3D> tileMeshes = new Dictionary<string, MeshInstance3D>();
PackedScene tileCollection = GD.Load<PackedScene>($"res://Assets/Objects/Tiles.glb");
Node root = tileCollection.Instantiate();
foreach (MeshInstance3D child in root.GetChildren())
{
tileMeshes.Add(child.Name.ToString().ToLower(), child);
}
return tileMeshes;
}
}
+1
View File
@@ -0,0 +1 @@
uid://cdhftg7wcgyis
+207
View File
@@ -0,0 +1,207 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Godot;
public class WFC
{
public static Dictionary<string, Dictionary<Direction, List<string>>> adjacency = new Dictionary<string, Dictionary<Direction, List<string>>>();
public static Random rand = new Random();
public enum Direction
{
Up,
Down,
Left,
Right,
None
}
public static readonly Vector2I[] offsets =
{
new Vector2I(0, -1),
new Vector2I(0, 1),
new Vector2I(-1, 0),
new Vector2I(1, 0)
};
public static readonly Direction[] dirs =
{
Direction.Up,
Direction.Down,
Direction.Left,
Direction.Right
};
public static Dictionary<string, HashSet<Direction>> tileConnections = new Dictionary<string, HashSet<Direction>>
{
["t_right"] = new() { Direction.Up, Direction.Down, Direction.Right },
["t_left"] = new() { Direction.Up, Direction.Down, Direction.Left },
["t_up"] = new() { Direction.Left, Direction.Right, Direction.Up },
["t_down"] = new() { Direction.Left, Direction.Right, Direction.Down },
["end_up"] = new() { Direction.Up },
["end_down"] = new() { Direction.Down },
["end_left"] = new() { Direction.Left },
["end_right"] = new() { Direction.Right },
["straight_left_right"] = new() { Direction.Left, Direction.Right },
["straight_up_down"] = new() { Direction.Up, Direction.Down },
["corner_up_left"] = new() { Direction.Up, Direction.Left },
["corner_up_right"] = new() { Direction.Up, Direction.Right },
["corner_down_left"] = new() { Direction.Down, Direction.Left },
["corner_down_right"] = new() { Direction.Down, Direction.Right },
["junction"] = new() { Direction.Up, Direction.Down, Direction.Left, Direction.Right },
["border"] = new() { }
};
public static Dictionary<string, float> weights = new()
{
["junction"] = 5f,
["t_up"] = 3f,
["t_down"] = 3f,
["t_left"] = 3f,
["t_right"] = 3f,
["straight_left_right"] = 2f,
["straight_up_down"] = 2f,
["corner_up_left"] = 0.5f,
["corner_up_right"] = 0.5f,
["corner_down_left"] = 0.5f,
["corner_down_right"] = 0.5f,
["end_up"] = 0.2f,
["end_down"] = 0.1f,
["end_left"] = 0.2f,
["end_right"] = 0.3f,
["border"] = 0.0f
};
public static Direction Opposite(Direction dir)
{
return dir switch
{
Direction.Up => Direction.Down,
Direction.Down => Direction.Up,
Direction.Left => Direction.Right,
Direction.Right => Direction.Left,
_ => dir
};
}
public static bool CanConnect(string a, string b, Direction direction, bool checkWalking)
{
var aDirs = tileConnections[a];
var bDirs = tileConnections[b];
bool aOpen = aDirs.Contains(direction);
bool bOpen = bDirs.Contains(Opposite(direction));
if (checkWalking) return aOpen && bOpen;
return aOpen == bOpen;
}
public static void FillAdjacencies()
{
foreach (var tile in tileConnections.Keys)
{
adjacency[tile] = new Dictionary<Direction, List<string>>();
foreach (Direction dir in Enum.GetValues(typeof(Direction)))
{
var valid = new List<string>();
foreach (var other in tileConnections.Keys)
{
if (CanConnect(tile, other, dir, false))
valid.Add(other);
}
adjacency[tile][dir] = valid;
}
}
}
public static bool IsWalkable(Tile tile)
{
var dirs = tileConnections[tile.collapsedMesh];
return dirs.Count > 0 && !dirs.Contains(Direction.None);
}
public static bool CanWalk(Tile[,] layer, Vector2I from, Vector2I to, Direction dir)
{
var fromTile = layer[from.X, from.Y];
var toTile = layer[to.X, to.Y];
if (!IsWalkable(toTile))
return false;
return CanConnect(fromTile.collapsedMesh, toTile.collapsedMesh, dir, true);
}
public static bool IsMapConnected(Tile[,] layer, float accessibilityThreshhold)
{
bool result = false;
HashSet<Vector2I> visited = new HashSet<Vector2I>();
List<Vector2I> toCheck = new List<Vector2I>();
Vector2I position;
toCheck.Add(new Vector2I(1, 1));
int safetyCounter = 0;
while (true)
{
if (toCheck.Count <= 0) break;
int index = rand.Next(toCheck.Count);
position = toCheck[index];
toCheck[index] = toCheck[^1];
toCheck.RemoveAt(toCheck.Count - 1);
if (!visited.Add(position)) continue;
for (int i = 0; i < 4; i++)
{
var next = position + offsets[i];
if (!InBounds(next, layer.GetLength(0), false))
continue;
if (CanWalk(layer, position, next, dirs[i]))
{
toCheck.Add(next);
}
}
safetyCounter++;
if (safetyCounter > layer.Length * 2) break;
if (visited.Count >= Math.Pow(layer.GetLength(0) - 1, 2) * accessibilityThreshhold)
{
result = true;
break;
}
}
if (safetyCounter > layer.Length * 2) GD.PrintErr("Loop too long");
return result;
}
public static bool InBounds(Vector2I pos, int layerSize, bool includeBorder = true)
{
if (includeBorder)
{
return pos.X >= 0 &&
pos.Y >= 0 &&
pos.X < layerSize &&
pos.Y < layerSize;
}
else
{
return pos.X > 0 &&
pos.Y > 0 &&
pos.X < layerSize - 1 &&
pos.Y < layerSize - 1;
}
}
}
+1
View File
@@ -0,0 +1 @@
uid://d3jw4gk5f8hhg
+73
View File
@@ -0,0 +1,73 @@
using Godot;
using System;
using System.Collections.Generic;
using System.Linq;
public partial class Tile
{
public Dictionary<string, MeshInstance3D> tileMeshes;
public string collapsedMesh;
Random rand = new Random();
public Vector3 Position;
public Vector2I GridPosition;
public void SetMeshes(Dictionary<string, MeshInstance3D> tileMeshes)
{
this.tileMeshes = new Dictionary<string, MeshInstance3D>(tileMeshes);
}
public string Collapse(string tile)
{
if (collapsedMesh != null) return "";
if (tileMeshes.Keys.Count <= 0) return "ERR";
collapsedMesh = (tile.Length > 0) ? tile : ChooseWeighted();
tileMeshes.Clear();
return collapsedMesh;
}
private string ChooseWeighted()
{
float totalWeight = 0f;
foreach (string tile in tileMeshes.Keys)
{
totalWeight += WFC.weights[tile];
}
float r = (float)(rand.NextDouble() * totalWeight);
float cumulative = 0f;
foreach (string tile in tileMeshes.Keys)
{
cumulative += WFC.weights[tile];
if (r <= cumulative)
return tile;
}
return "junction";
}
public int Propagate(HashSet<string> possibleKeys)
{
int amountRemoved = 0;
if (collapsedMesh != null) return 0;
foreach (string key in tileMeshes.Keys.ToList())
{
if (!possibleKeys.Contains(key))
{
tileMeshes.Remove(key);
amountRemoved++;
}
}
if (tileMeshes.Count == 0) return int.MaxValue;
return amountRemoved;
}
public void Reset(Dictionary<string, MeshInstance3D> tileMeshes)
{
collapsedMesh = null;
SetMeshes(tileMeshes);
}
}
+1
View File
@@ -0,0 +1 @@
uid://crimbrc78gxkc
+110
View File
@@ -0,0 +1,110 @@
using Godot;
using System;
using System.Collections.Generic;
using System.Linq;
using static GameData;
public partial class World : Node3D
{
public Dictionary<string, MeshInstance3D> tileMeshes;
PackedScene layerPrefab = ResourceLoader.LoadLayerPrefab();
private Dictionary<string, MultiMeshInstance3D> multiMeshes = new();
private Dictionary<string, Mesh> meshLibrary = new();
Layer[] map;
Layer layerNode;
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
WFC.FillAdjacencies();
tileMeshes = ResourceLoader.LoadTiles();
foreach (var kvp in tileMeshes)
{
var temp = kvp.Value;
meshLibrary[kvp.Key] = temp.Mesh;
temp.QueueFree();
}
foreach (var kvp in meshLibrary)
{
var mm = new MultiMesh();
mm.Mesh = kvp.Value;
mm.TransformFormat = MultiMesh.TransformFormatEnum.Transform3D;
var instance = new MultiMeshInstance3D();
instance.Multimesh = mm;
AddChild(instance);
multiMeshes[kvp.Key] = instance;
}
map = new Layer[ruinSize];
GenerateWorld();
GD.Print("World generated");
BuildMeshesForLayer(0);
}
// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
if (Input.IsActionJustPressed("layer_up") && currentLayer > 0) currentLayer--;
if (Input.IsActionJustPressed("layer_down") && currentLayer < ruinSize - 1) currentLayer++;
if (currentLayer != visibleLayer)
{
BuildMeshesForLayer(currentLayer);
visibleLayer = currentLayer;
}
}
private void GenerateWorld()
{
DateTime now = DateTime.Now;
for (int layer = 0; layer < ruinSize; layer++)
{
layerNode = layerPrefab.Instantiate<Layer>();
AddChild(layerNode);
layerNode.SetupLayer(layerSize, layer, tileMeshes);
map[layer] = layerNode;
}
GD.Print("Time for map generation: " + (DateTime.Now - now).Seconds);
}
private void BuildMeshesForLayer(int layerIndex)
{
foreach (MultiMeshInstance3D mm in multiMeshes.Values)
{
mm.Multimesh.InstanceCount = 0;
}
Layer layer = map[layerIndex];
Dictionary<string, List<Transform3D>> batches = new();
for (int x = 0; x < layerSize; x++)
{
for (int y = 0; y < layerSize; y++)
{
Tile tile = layer.tiles[x, y];
string key = tile.collapsedMesh;
if (!batches.ContainsKey(key))
batches[key] = new List<Transform3D>();
batches[key].Add(new Transform3D(
Basis.Identity,
tile.Position
));
}
}
foreach (var kvp in batches)
{
MultiMesh mm = multiMeshes[kvp.Key].Multimesh;
List<Transform3D> list = kvp.Value;
mm.InstanceCount = list.Count;
for (int i = 0; i < list.Count; i++)
{
mm.SetInstanceTransform(i, list[i]);
}
}
}
}
+1
View File
@@ -0,0 +1 @@
uid://br2udyi6t8yvf