From 7e393e88ac14cbded4ceb328fc0b9a8a4d0a381c Mon Sep 17 00:00:00 2001 From: Kayne Ruse Date: Thu, 8 Jan 2026 13:20:03 +1100 Subject: [PATCH] This works reasonably fast for 16x16 chunks --- client.gd | 8 +++-- wfc/generator.gd | 84 ++++++++++++++++++++++++++++++++---------------- 2 files changed, 62 insertions(+), 30 deletions(-) diff --git a/client.gd b/client.gd index dfaeed0..dcf0a66 100644 --- a/client.gd +++ b/client.gd @@ -4,9 +4,9 @@ extends Node var _chunks: Array[Chunk] = [] #Create some test samples - these samples must ALWAYS be 9-elements long, 5th element is the result -var _samples: Array[PackedInt32Array] = [ - [1,1,1, 1,1,1, 1,1,1], -] +#var _samples: Array[PackedInt32Array] = [ + #[1,1,1, 1,1,1, 1,1,1], +#] @onready var wfc: Node = get_node("../Generator") @@ -15,6 +15,8 @@ func _ready() -> void: var ruleset: PackedInt32Array = read_sample_ruleset("sample1.png") var samples: Array[PackedInt32Array] = parse_samples_from_ruleset(ruleset) var c = wfc.generate_chunk_at(0,0,_chunks,samples) + #TODO: build a visual layout for the results + #TODO: handle the chunk-edges #print(ruleset) #print(samples) diff --git a/wfc/generator.gd b/wfc/generator.gd index 05633bb..ffde457 100644 --- a/wfc/generator.gd +++ b/wfc/generator.gd @@ -1,48 +1,78 @@ extends Node ## The generator works with pure numbers, you'll need something else to convert it to actual tiles -## Makes a new chunk, derived from the given WFC samples -func generate_chunk_at(_x: int, _y: int, _chunk_array: Array[Chunk], _samples: Array[PackedInt32Array]) -> Chunk: - #TODO: fix the chunk-edges - var chunk: Chunk = Chunk.new(_x, _y) - - while _set_lowest_entropy_tile(chunk, _samples) > 0: pass - - _chunk_array.append(chunk) - return chunk +## Entropy for tiles far away from activity shouldn't be recalculated, so cache them +var entropy_valid: Array[Array] = [] ##Options for each tile +var entropy_total: PackedInt32Array = [] ##Total number of options, negative means unknown -## Returns the entropy of the selected tile -func _set_lowest_entropy_tile(chunk: Chunk, _samples: Array[PackedInt32Array]) -> int: - var entropy: Array[Array] = [] - entropy.resize(Chunk.CHUNK_WIDTH * Chunk.CHUNK_HEIGHT) +func _reset_entropy() -> void: + entropy_valid.clear() + entropy_valid.resize(Chunk.CHUNK_WIDTH * Chunk.CHUNK_HEIGHT) + entropy_valid.fill([]) + entropy_total.clear() + entropy_total.resize(Chunk.CHUNK_WIDTH * Chunk.CHUNK_HEIGHT) + entropy_total.fill(-1) - #iterate over unset tiles with valid neighbours +func _update_entropy(chunk: Chunk, samples: Array[PackedInt32Array]) -> void: + #iterate over indeterminate tiles to find their valid neighbours for x in range(Chunk.CHUNK_WIDTH): for y in range((Chunk.CHUNK_HEIGHT)): - if chunk.data[y * Chunk.CHUNK_HEIGHT + x] == 0: #direct access, to skip extra checks - entropy[y * Chunk.CHUNK_HEIGHT + x] = _find_valid_samples(chunk, x, y, _samples) + if entropy_total[y * Chunk.CHUNK_WIDTH + x] < 0: #There's some excess multiplication in this code, but I want to get it right first + entropy_valid[y * Chunk.CHUNK_WIDTH + x] = _find_valid_samples_at(chunk, x, y, samples) #this overrides any pre-existing cached data + entropy_total[y * Chunk.CHUNK_WIDTH + x] = entropy_valid[y * Chunk.CHUNK_WIDTH + x].size() #the entropy is stored separately or faster processing + +func _clear_entropy_at(index: int) -> void: + var clear_at := func (dx: int, dy: int) -> void: + var check = index + (dy * Chunk.CHUNK_WIDTH) + (dx) + if check >= 0 and check < Chunk.CHUNK_WIDTH * Chunk.CHUNK_HEIGHT: + if entropy_total[check] > 0: entropy_total[check] = -1 #unset surrounding tiles + clear_at.call(-1, -1) + clear_at.call( 0, -1) + clear_at.call( 1, -1) + clear_at.call(-1, 0) + entropy_total[index] = 0 #shortcut + clear_at.call( 1, 0) + clear_at.call(-1, +1) + clear_at.call( 0, +1) + clear_at.call( 1, +1) + +## Creates a new chunk, derived from the given WFC samples +func generate_chunk_at(_x: int, _y: int, chunk_array: Array[Chunk], samples: Array[PackedInt32Array]) -> Chunk: + var chunk: Chunk = Chunk.new(_x, _y) + _reset_entropy() + _update_entropy(chunk, samples) + while true: + var index = _find_lowest_entropy_tile_index() + if index < 0: break #none found, finished + + var s: PackedInt32Array = entropy_valid[index].pick_random() + chunk.data[index] = s[4] + + _clear_entropy_at(index) + _update_entropy(chunk, samples) + + chunk_array.append(chunk) + return chunk + +## Returns the index of a tile with the lowest entropy, without being zero +func _find_lowest_entropy_tile_index() -> int: #find the lowest-entropy tile var lowest: int = -1 + for i in range(Chunk.CHUNK_WIDTH * Chunk.CHUNK_HEIGHT): - if entropy[i].size() == 0: continue #no options - if lowest < 0: + if entropy_total[i] <= 0: continue #no options lowest = i continue - if entropy[i].size() < entropy[lowest].size(): + + if entropy_total[i] > 0 and entropy_total[i] < entropy_total[lowest]: lowest = i #finished - if lowest < 0: return lowest - - #finally, set the tile from the sample - var s: PackedInt32Array = entropy[lowest].pick_random() - chunk.data[lowest] = s[4] - return entropy.size() + return lowest -## Returns the valid samples -func _find_valid_samples(chunk: Chunk, tile_x: int, tile_y: int, _samples: Array[PackedInt32Array]) -> Array[PackedInt32Array]: +func _find_valid_samples_at(chunk: Chunk, tile_x: int, tile_y: int, _samples: Array[PackedInt32Array]) -> Array[PackedInt32Array]: var valid: Array[PackedInt32Array] = [] #use a lambda for easy reading below