@ -0,0 +1,34 @@ | |||
[remap] | |||
importer="texture" | |||
type="CompressedTexture2D" | |||
uid="uid://b8gggrriib8n" | |||
path="res://.godot/imported/refresh.png-01865d575294ccf6716390f339aefb03.ctex" | |||
metadata={ | |||
"vram_texture": false | |||
} | |||
[deps] | |||
source_file="res://assets/refresh.png" | |||
dest_files=["res://.godot/imported/refresh.png-01865d575294ccf6716390f339aefb03.ctex"] | |||
[params] | |||
compress/mode=0 | |||
compress/high_quality=false | |||
compress/lossy_quality=0.7 | |||
compress/hdr_compression=1 | |||
compress/normal_map=0 | |||
compress/channel_pack=0 | |||
mipmaps/generate=false | |||
mipmaps/limit=-1 | |||
roughness/mode=0 | |||
roughness/src_normal="" | |||
process/fix_alpha_border=true | |||
process/premult_alpha=false | |||
process/normal_map_invert_y=false | |||
process/hdr_as_srgb=false | |||
process/hdr_clamp_exposure=false | |||
process/size_limit=0 | |||
detect_3d/compress_to=1 |
@ -0,0 +1,21 @@ | |||
extends Camera2D | |||
@export var move_speed: int = 250 | |||
@export var zoom_increment: Vector2 = Vector2(0.1, 0.1) | |||
func _process(delta: float) -> void: | |||
var input_dir := Input.get_vector("Left", "Right", "Up", "Down") | |||
var velocity_y = input_dir.y * move_speed * delta | |||
var velocity_x = input_dir.x * move_speed * delta | |||
position += Vector2(velocity_x, velocity_y) | |||
func _unhandled_input(event: InputEvent) -> void: | |||
if event.is_action_pressed("Reset Zoom"): | |||
zoom = Vector2.ONE | |||
if event.is_action_pressed("Zoom In"): | |||
zoom += zoom_increment | |||
if event.is_action_pressed("Zoom Out"): | |||
zoom -= zoom_increment |
@ -0,0 +1,308 @@ | |||
## Conway's Game of Life | |||
## Rule 1 - Any live cell with fewer than two live neighbours dies (underpopulation) | |||
## Rule 2 - Any live cell with two or three live neighbours lives | |||
## Rule 3 - Any live cell with more than three live neighbours dies (overpopulation) | |||
## Rule 4 - Any dead cell with exactly three live neighbours becomes a live cell (reproduction) | |||
extends Node2D | |||
enum CellStates { | |||
DEAD, | |||
ALIVE, | |||
} | |||
## UI | |||
@onready var background_ui: Control = $UI/Background | |||
@onready var debug_ui: MarginContainer = $UI/Debug | |||
@onready var debug_generation_counter: Label = $UI/Debug/VBoxContainer/GenerationCounter | |||
@onready var debug_living_cells_counter: Label = $UI/Debug/VBoxContainer/LivingCellsCounter | |||
@onready var debug_world_seed: Label = $UI/Debug/VBoxContainer/WorldSeed | |||
@onready var generation_ui: MarginContainer = $UI/WorldGeneration | |||
@onready var generation_seed: LineEdit = $UI/WorldGeneration/VBoxContainer/Seed/Input | |||
@onready var generation_speed: HSlider = $UI/WorldGeneration/VBoxContainer/GenerationSpeed/Input | |||
@onready var generation_world_size_x: LineEdit = $UI/WorldGeneration/VBoxContainer/WorldSize/Input_x | |||
@onready var generation_world_size_y: LineEdit = $UI/WorldGeneration/VBoxContainer/WorldSize/Input_y | |||
@onready var start_paused_button: CheckBox = $UI/WorldGeneration/VBoxContainer/FinalRow/StartPausedButton | |||
@onready var messages_label: Label = $UI/Messages/Label | |||
## Other | |||
@onready var camera: Camera2D = $Camera | |||
@onready var generation_timer: Timer = $GenerationTimer | |||
@export var world_seed: int ## The seed utilized for generation of the world | |||
@export var cell_size: Vector2 = Vector2(16, 16) ## Size of each cell image | |||
@export var cell_texture: Texture2D ## Testure for each cell | |||
@export var world_size: Vector2 = Vector2(32, 32) ## The size of the world | |||
var cell_instance | |||
var evolution_is_stalled: bool = false | |||
var generation: int = 1 | |||
var is_paused: bool = false | |||
var total_living: int = 0 | |||
var cell_ids: Array = [] # Store cell RIDs | |||
var cell_states: Array = [] | |||
var previous_generation_hashes: Array = [] # Store the generation related to the hashes | |||
var previous_generation_count: int = 10 # Number of generations to check for evolution stalling | |||
# Number of collisions to consider evolution stalled | |||
# The higher the number, the less likely to catch oscillators | |||
# Consider the previous_generation_count as this may need to increase for larger counts | |||
# 4 Collisions out of 10 previous_generation_count seems reasonable although it may not be quite enough | |||
var collision_count_limit: int = 4 | |||
var timer_speeds: Array = [0.1, 0.08, 0.05, 0.03, 0.01] # The speeds for the generation_timer for selection from the UI | |||
func _ready() -> void: | |||
update_generation_ui() | |||
debug_ui.visible = false | |||
background_ui.visible = true | |||
generation_ui.visible = true | |||
func _input(event: InputEvent) -> void: | |||
if event.is_action_pressed("Pause"): | |||
toggle_pause() | |||
if is_paused and event.is_action_pressed("Next Generation"): | |||
process_generation() | |||
debug_generation_counter.text = "Generation: %s" % generation | |||
debug_living_cells_counter.text = "Living Cells: %s" % total_living | |||
if event.is_action_pressed("Menu"): | |||
toggle_pause() | |||
update_generation_ui() | |||
generation_ui.visible = is_paused | |||
# | |||
# UI | |||
# | |||
## Update the Generation UI with the current settings | |||
func update_generation_ui() -> void: | |||
if world_seed: | |||
generation_seed.text = str(world_seed) | |||
else: | |||
generation_seed.text = str(randi()) | |||
generation_world_size_x.text = str(world_size.x) | |||
generation_world_size_y.text = str(world_size.y) | |||
start_paused_button.button_pressed = false | |||
# | |||
# Conway Specific | |||
# | |||
func start_conway() -> void: | |||
clear_world() | |||
generate_world() | |||
update_previous_generation_hashes() | |||
generation = 1 | |||
evolution_is_stalled = false | |||
debug_world_seed.text = "World Seeed: %s" % world_seed | |||
debug_generation_counter.text = "Generation: %s" % generation | |||
debug_living_cells_counter.text = "Living Cells: %s" % total_living | |||
# Center the camera on the world | |||
camera.position = Vector2( | |||
world_size.x * cell_size.x / 2, | |||
world_size.y * cell_size.y / 2 | |||
) | |||
camera.zoom = Vector2.ONE | |||
if not is_paused: generation_timer.start() | |||
## Check a cell against the Conway rules and return True if cell should be alive | |||
## The logic in this could be cleaned up pretty easily. Only verbose for understanding. | |||
func cell_is_alive(pos: Vector2) -> bool: | |||
var neighbors := count_living_neighbors(pos) | |||
var currently_alive = cell_states[pos.x][pos.y] == CellStates.ALIVE | |||
# Rule 1 - Any live cell with fewer than two live neighbours dies (underpopulation) | |||
if currently_alive and neighbors < 2: | |||
return false | |||
# Rule 2 - Any live cell with two or three live neighbours lives | |||
elif currently_alive and (neighbors == 2 or neighbors == 3): | |||
return true | |||
# Rule 3 - Any live cell with more than three live neighbours dies (overpopulation) | |||
elif currently_alive and neighbors > 3: | |||
return false | |||
# Rule 4 - Any dead cell with exactly three live neighbours becomes a live cell (reproduction) | |||
elif not currently_alive and neighbors == 3: | |||
return true | |||
return false | |||
## Count all neighbors surrounding a cell | |||
func count_living_neighbors(pos: Vector2) -> int: | |||
var count := 0 | |||
@warning_ignore("incompatible_ternary") | |||
var x_min = 0 if pos.x - 1 < 0 else pos.x - 1 | |||
@warning_ignore("incompatible_ternary") | |||
var y_min = 0 if pos.y - 1 < 0 else pos.y - 1 | |||
var x_max = world_size.x if pos.x + 2 > world_size.x else pos.x + 2 | |||
var y_max = world_size.y if pos.y + 2 > world_size.y else pos.y + 2 | |||
for x in range(x_min, x_max): | |||
for y in range(y_min, y_max): | |||
if x == pos.x and y == pos.y: continue # Current cell - Don't count | |||
if cell_states[x][y] == CellStates.ALIVE: | |||
count += 1 | |||
return count | |||
func end_world_generation() -> void: | |||
# Determine which generation first stalled | |||
var current_world_hash: int = cell_states.hash() | |||
var colliding = previous_generation_hashes.filter(func(gen): return gen.hash == current_world_hash) | |||
var stalled_generation = colliding[0].generation | |||
messages_label.text = "Detected Stalled Evolution Starting at Generation %s" % stalled_generation | |||
messages_label.visible = true | |||
evolution_is_stalled = true | |||
## Get the number of previous generation results that collide with the current hash | |||
func get_colliding_hash_count(input_hash: int) -> int: | |||
var count = 0 | |||
for gen in previous_generation_hashes: | |||
count += int(gen.hash == input_hash) | |||
return count | |||
## Loop through world to generate cell states, hide/show cells depending on state | |||
func process_generation() -> void: | |||
if evolution_is_stalled: return | |||
update_previous_generation_hashes() | |||
if previous_generation_hashes[previous_generation_hashes.size()-1].collisions >= collision_count_limit: | |||
return end_world_generation() | |||
generation += 1 | |||
total_living = 0 | |||
var rs := RenderingServer | |||
var new_states: Array= [] | |||
for x in range(world_size.x): | |||
new_states.append([]) | |||
new_states[x].resize(world_size.x) | |||
for y in range(world_size.y): | |||
var pos = Vector2(x, y) | |||
var is_alive = cell_is_alive(pos) | |||
total_living += int(is_alive) | |||
new_states[x][y] = int(is_alive) | |||
rs.canvas_item_set_visible(cell_ids[x][y], is_alive) | |||
cell_states = new_states | |||
## Update the previous run hash and generation ids | |||
## We only store the last `previous_generation_count` number of results | |||
func update_previous_generation_hashes() -> void: | |||
if previous_generation_hashes.size() < previous_generation_count: | |||
previous_generation_hashes.resize(previous_generation_count) | |||
previous_generation_hashes.fill({"generation": 0, "hash": 0, "collisions": 0}) | |||
# Remove the oldest generation information | |||
previous_generation_hashes.remove_at(0) | |||
var generation_hash := cell_states.hash() | |||
previous_generation_hashes.append({ | |||
"generation": generation, | |||
"hash": generation_hash, | |||
"collisions": get_colliding_hash_count(generation_hash), | |||
}) | |||
## Toggle Pause UI and Start/Stop Generation Timer | |||
func toggle_pause() -> void: | |||
is_paused = !is_paused | |||
background_ui.visible = is_paused | |||
if is_paused: generation_timer.stop() | |||
else: generation_timer.start() | |||
## Loop over cell_ids and free the resource in the RenderingServer | |||
## Clear cell_ids and cell_states | |||
func clear_world() -> void: | |||
for row in cell_ids: | |||
for id in row: | |||
RenderingServer.free_rid(id) | |||
cell_ids = [] | |||
cell_states = [] | |||
## Create the cell using the rendering server | |||
## This is only performed on initial world generation | |||
func create_cell(pos: Vector2, visible_cell: bool = true) -> RID: | |||
var rs = RenderingServer | |||
cell_instance = rs.canvas_item_create() | |||
rs.canvas_item_set_parent(cell_instance, get_canvas_item()) | |||
var rect: Rect2 = Rect2(-cell_size.x/2, -cell_size.y/2, cell_size.x, cell_size.y) # Centered with cell size | |||
rs.canvas_item_add_texture_rect(cell_instance, rect, cell_texture) | |||
var pos_fixed = Vector2(pos.x * cell_size.x, pos.y * cell_size.y) | |||
var trans = Transform2D(0, pos_fixed) | |||
rs.canvas_item_set_transform(cell_instance, trans) | |||
rs.canvas_item_set_visible(cell_instance, visible_cell) | |||
return cell_instance | |||
## Generate the world with cells in random states (alive or dead) | |||
func generate_world() -> void: | |||
for x in range(world_size.x): | |||
cell_ids.append([]) | |||
cell_ids[x].resize(world_size.x) | |||
cell_states.append([]) | |||
cell_states[x].resize(world_size.x) | |||
for y in range(world_size.y): | |||
var is_alive: int = randi_range(CellStates.DEAD, CellStates.ALIVE) | |||
total_living += is_alive | |||
cell_states[x][y] = is_alive | |||
cell_ids[x][y] = create_cell(Vector2(x, y), bool(is_alive)) | |||
func _on_generation_timer_timeout() -> void: | |||
process_generation() | |||
debug_generation_counter.text = "Generation: %s" % generation | |||
debug_living_cells_counter.text = "Living Cells: %s" % total_living | |||
generation_timer.start() | |||
func _on_generation_speed_drag_ended(value_changed: bool) -> void: | |||
var speed = generation_speed.value - 1 | |||
if value_changed: | |||
generation_timer.wait_time = timer_speeds[speed] | |||
func _on_quit_button_pressed() -> void: | |||
get_tree().quit() | |||
func _on_run_button_pressed() -> void: | |||
if generation_seed.text.strip_edges(): | |||
world_seed = int(generation_seed.text) | |||
else: | |||
world_seed = randi() | |||
world_size = Vector2(int(generation_world_size_x.text), int(generation_world_size_y.text)) | |||
debug_ui.visible = true | |||
generation_ui.visible = false | |||
background_ui.visible = false | |||
messages_label.visible = false | |||
seed(world_seed) | |||
start_conway() | |||
func _on_start_paused_button_toggled(button_pressed: bool) -> void: | |||
is_paused = button_pressed | |||
func _on_world_seed_generate_pressed() -> void: | |||
generation_seed.text = str(randi()) | |||
func _exit_tree() -> void: | |||
clear_world() |
@ -0,0 +1,206 @@ | |||
[gd_scene load_steps=5 format=3 uid="uid://d3twfk56sjf2m"] | |||
[ext_resource type="Script" path="res://scenes/conway.gd" id="1_1gvb0"] | |||
[ext_resource type="Texture2D" uid="uid://c2vm5pfsamed4" path="res://assets/icon.svg" id="2_l6fa6"] | |||
[ext_resource type="Script" path="res://scenes/camera.gd" id="3_e0v0d"] | |||
[ext_resource type="Texture2D" uid="uid://b8gggrriib8n" path="res://assets/refresh.png" id="4_hmfh6"] | |||
[node name="World" type="Node2D"] | |||
position = Vector2(-152, 0) | |||
script = ExtResource("1_1gvb0") | |||
cell_texture = ExtResource("2_l6fa6") | |||
[node name="Camera" type="Camera2D" parent="."] | |||
position = Vector2(464, 200) | |||
script = ExtResource("3_e0v0d") | |||
[node name="GenerationTimer" type="Timer" parent="."] | |||
wait_time = 0.05 | |||
one_shot = true | |||
[node name="UI" type="CanvasLayer" parent="."] | |||
[node name="Background" type="Control" parent="UI"] | |||
layout_mode = 3 | |||
anchors_preset = 15 | |||
anchor_right = 1.0 | |||
anchor_bottom = 1.0 | |||
grow_horizontal = 2 | |||
grow_vertical = 2 | |||
[node name="ColorRect" type="ColorRect" parent="UI/Background"] | |||
layout_mode = 1 | |||
anchors_preset = 15 | |||
anchor_right = 1.0 | |||
anchor_bottom = 1.0 | |||
grow_horizontal = 2 | |||
grow_vertical = 2 | |||
color = Color(0, 0, 0, 0.12549) | |||
[node name="Debug" type="MarginContainer" parent="UI"] | |||
visible = false | |||
offset_right = 106.0 | |||
offset_bottom = 116.0 | |||
[node name="VBoxContainer" type="VBoxContainer" parent="UI/Debug"] | |||
layout_mode = 2 | |||
[node name="WorldSeed" type="Label" parent="UI/Debug/VBoxContainer"] | |||
layout_mode = 2 | |||
text = "World Seed: 0" | |||
[node name="GenerationCounter" type="Label" parent="UI/Debug/VBoxContainer"] | |||
layout_mode = 2 | |||
text = "Generation: 1" | |||
[node name="LivingCellsCounter" type="Label" parent="UI/Debug/VBoxContainer"] | |||
layout_mode = 2 | |||
text = "Living Cells: 0" | |||
[node name="WorldGeneration" type="MarginContainer" parent="UI"] | |||
anchors_preset = 8 | |||
anchor_left = 0.5 | |||
anchor_top = 0.5 | |||
anchor_right = 0.5 | |||
anchor_bottom = 0.5 | |||
offset_left = -173.0 | |||
offset_top = -78.0 | |||
offset_right = 173.0 | |||
offset_bottom = 78.0 | |||
grow_horizontal = 2 | |||
grow_vertical = 2 | |||
size_flags_horizontal = 4 | |||
size_flags_vertical = 4 | |||
theme_override_constants/margin_left = 10 | |||
theme_override_constants/margin_top = 10 | |||
theme_override_constants/margin_right = 10 | |||
theme_override_constants/margin_bottom = 10 | |||
[node name="VBoxContainer" type="VBoxContainer" parent="UI/WorldGeneration"] | |||
layout_mode = 2 | |||
[node name="Seed" type="HBoxContainer" parent="UI/WorldGeneration/VBoxContainer"] | |||
layout_mode = 2 | |||
theme_override_constants/separation = 6 | |||
[node name="Label" type="Label" parent="UI/WorldGeneration/VBoxContainer/Seed"] | |||
custom_minimum_size = Vector2(120, 0) | |||
layout_mode = 2 | |||
text = "Seed" | |||
[node name="Generate" type="Button" parent="UI/WorldGeneration/VBoxContainer/Seed"] | |||
self_modulate = Color(0.501961, 0.501961, 0.501961, 1) | |||
layout_mode = 2 | |||
icon = ExtResource("4_hmfh6") | |||
[node name="Input" type="LineEdit" parent="UI/WorldGeneration/VBoxContainer/Seed"] | |||
custom_minimum_size = Vector2(200, 0) | |||
layout_mode = 2 | |||
placeholder_text = "Empty for Random" | |||
clear_button_enabled = true | |||
select_all_on_focus = true | |||
[node name="WorldSize" type="HBoxContainer" parent="UI/WorldGeneration/VBoxContainer"] | |||
layout_mode = 2 | |||
theme_override_constants/separation = 6 | |||
[node name="Label" type="Label" parent="UI/WorldGeneration/VBoxContainer/WorldSize"] | |||
custom_minimum_size = Vector2(120, 0) | |||
layout_mode = 2 | |||
text = "World Size" | |||
[node name="Padding" type="VSeparator" parent="UI/WorldGeneration/VBoxContainer/WorldSize"] | |||
modulate = Color(1, 1, 1, 0) | |||
layout_mode = 2 | |||
size_flags_horizontal = 3 | |||
[node name="Label_x" type="Label" parent="UI/WorldGeneration/VBoxContainer/WorldSize"] | |||
layout_mode = 2 | |||
text = "X:" | |||
[node name="Input_x" type="LineEdit" parent="UI/WorldGeneration/VBoxContainer/WorldSize"] | |||
custom_minimum_size = Vector2(20, 0) | |||
layout_mode = 2 | |||
text = "32" | |||
max_length = 4 | |||
select_all_on_focus = true | |||
[node name="Label_y" type="Label" parent="UI/WorldGeneration/VBoxContainer/WorldSize"] | |||
layout_mode = 2 | |||
text = "Y:" | |||
[node name="Input_y" type="LineEdit" parent="UI/WorldGeneration/VBoxContainer/WorldSize"] | |||
custom_minimum_size = Vector2(20, 0) | |||
layout_mode = 2 | |||
text = "32" | |||
max_length = 4 | |||
select_all_on_focus = true | |||
[node name="GenerationSpeed" type="HBoxContainer" parent="UI/WorldGeneration/VBoxContainer"] | |||
layout_mode = 2 | |||
[node name="Label" type="Label" parent="UI/WorldGeneration/VBoxContainer/GenerationSpeed"] | |||
layout_mode = 2 | |||
text = "Generation Speed" | |||
[node name="Padding" type="VSeparator" parent="UI/WorldGeneration/VBoxContainer/GenerationSpeed"] | |||
modulate = Color(1, 1, 1, 0) | |||
layout_mode = 2 | |||
size_flags_horizontal = 3 | |||
[node name="Input" type="HSlider" parent="UI/WorldGeneration/VBoxContainer/GenerationSpeed"] | |||
custom_minimum_size = Vector2(200, 0) | |||
layout_mode = 2 | |||
tooltip_text = "The higher the speed, the less time between generations. | |||
Slowest: 0.1/s | |||
Fastest: 0.01/s" | |||
min_value = 1.0 | |||
max_value = 5.0 | |||
value = 3.0 | |||
tick_count = 5 | |||
ticks_on_borders = true | |||
[node name="FinalRow" type="HBoxContainer" parent="UI/WorldGeneration/VBoxContainer"] | |||
layout_mode = 2 | |||
[node name="StartPausedButton" type="CheckBox" parent="UI/WorldGeneration/VBoxContainer/FinalRow"] | |||
layout_mode = 2 | |||
text = "Start Paused" | |||
[node name="Padding" type="VSeparator" parent="UI/WorldGeneration/VBoxContainer/FinalRow"] | |||
modulate = Color(1, 1, 1, 0) | |||
layout_mode = 2 | |||
size_flags_horizontal = 3 | |||
[node name="RunButton" type="Button" parent="UI/WorldGeneration/VBoxContainer/FinalRow"] | |||
layout_mode = 2 | |||
text = "Run" | |||
[node name="QuitButton" type="Button" parent="UI/WorldGeneration/VBoxContainer/FinalRow"] | |||
layout_direction = 3 | |||
layout_mode = 2 | |||
text = "Quit" | |||
[node name="Messages" type="MarginContainer" parent="UI"] | |||
anchors_preset = 7 | |||
anchor_left = 0.5 | |||
anchor_top = 1.0 | |||
anchor_right = 0.5 | |||
anchor_bottom = 1.0 | |||
offset_left = -20.0 | |||
offset_top = -40.0 | |||
offset_right = 20.0 | |||
grow_horizontal = 2 | |||
grow_vertical = 0 | |||
[node name="Label" type="Label" parent="UI/Messages"] | |||
visible = false | |||
layout_mode = 2 | |||
[connection signal="timeout" from="GenerationTimer" to="." method="_on_generation_timer_timeout"] | |||
[connection signal="pressed" from="UI/WorldGeneration/VBoxContainer/Seed/Generate" to="." method="_on_world_seed_generate_pressed"] | |||
[connection signal="drag_ended" from="UI/WorldGeneration/VBoxContainer/GenerationSpeed/Input" to="." method="_on_generation_speed_drag_ended"] | |||
[connection signal="toggled" from="UI/WorldGeneration/VBoxContainer/FinalRow/StartPausedButton" to="." method="_on_start_paused_button_toggled"] | |||
[connection signal="pressed" from="UI/WorldGeneration/VBoxContainer/FinalRow/RunButton" to="." method="_on_run_button_pressed"] | |||
[connection signal="pressed" from="UI/WorldGeneration/VBoxContainer/FinalRow/QuitButton" to="." method="_on_quit_button_pressed"] |
@ -1,10 +0,0 @@ | |||
extends Label | |||
@export var include_label: bool = true | |||
@onready var label_prepend: String = "FPS: " if include_label else "" | |||
func _process(_delta: float) -> void: | |||
text = label_prepend + str(Engine.get_frames_per_second()) |
@ -1,8 +0,0 @@ | |||
[gd_scene load_steps=2 format=3 uid="uid://cy6vsgu8o0rad"] | |||
[ext_resource type="Script" path="res://scenes/fps_counter/fps_counter.gd" id="1_vn43a"] | |||
[node name="FPSCounter" type="Label"] | |||
offset_right = 40.0 | |||
offset_bottom = 23.0 | |||
script = ExtResource("1_vn43a") |
@ -1,144 +0,0 @@ | |||
## Conway's Game of Life | |||
## Rule 1 - Any live cell with fewer than two live neighbours dies (underpopulation) | |||
## Rule 2 - Any live cell with two or three live neighbours lives | |||
## Rule 3 - Any live cell with more than three live neighbours dies (overpopulation) | |||
## Rule 4 - Any dead cell with epos.xactly three live neighbours becomes a live cell (reproduction) | |||
extends Node2D | |||
enum CellStates { | |||
DEAD, | |||
ALIVE, | |||
} | |||
@onready var camera: Camera2D = $Camera2D | |||
@onready var generation_counter: Label = $CanvasLayer/Debug/VBoxContainer/GenerationCounter | |||
@onready var generation_timer: Timer = $GenerationTimer | |||
@onready var living_cells_counter: Label = $CanvasLayer/Debug/VBoxContainer/LivingCellsCounter | |||
@onready var world_seed_label: Label = $CanvasLayer/Debug/VBoxContainer/WorldSeed | |||
@export var world_seed: int | |||
@export var cell_size: Vector2 = Vector2(16, 16) | |||
@export var cell_texture: Texture2D | |||
@export var world_size: Vector2 = Vector2(32, 32) | |||
var cell_instance | |||
var generation: int = 1 | |||
var total_living: int = 0 | |||
var world: Array = [] | |||
func _ready() -> void: | |||
if not world_seed: | |||
world_seed = randi() | |||
seed(world_seed) | |||
world_seed_label.text = "World Seeed: %s" % world_seed | |||
generate_world() | |||
# Center the camera on the world | |||
camera.position.x = world_size.x * cell_size.x / 2 | |||
camera.position.y = world_size.y * cell_size.y / 2 | |||
## Check a cell against the Conway rules and return True if cell shoudl be alive | |||
func cell_is_alive(pos: Vector2) -> bool: | |||
var neighbors := count_living_neighbors(pos) | |||
var currently_alive = world[pos.x][pos.y] is RID | |||
# Rule 1 - Any live cell with fewer than two live neighbours dies (underpopulation) | |||
if currently_alive and neighbors < 2: | |||
return false | |||
# Rule 2 - Any live cell with two or three live neighbours lives | |||
elif currently_alive and (neighbors == 2 or neighbors == 3): | |||
return true | |||
# Rule 3 - Any live cell with more than three live neighbours dies (overpopulation) | |||
elif currently_alive and neighbors > 3: | |||
return false | |||
# Rule 4 - Any dead cell with epos.xactly three live neighbours becomes a live cell (reproduction) | |||
elif not currently_alive and neighbors == 3: | |||
return true | |||
return false | |||
## Count all neighbors surrounding a cell | |||
func count_living_neighbors(pos: Vector2) -> int: | |||
var count := 0 | |||
var x_min = 0 if pos.x - 1 < 0 else pos.x - 1 | |||
var y_min = 0 if pos.y - 1 < 0 else pos.y - 1 | |||
var x_max = world_size.x if pos.x + 2 > world_size.x else pos.x + 2 | |||
var y_max = world_size.y if pos.y + 2 > world_size.y else pos.y + 2 | |||
for x in range(x_min, x_max): | |||
for y in range(y_min, y_max): | |||
if x == pos.x and y == pos.y: continue # Current cell - Don't count | |||
if world[x][y] is RID: | |||
count += 1 | |||
return count | |||
## Loop through the world and create or kill cells depending on rules | |||
func process_generation() -> void: | |||
generation += 1 | |||
total_living = 0 | |||
var new_world: Array= [] | |||
for x in range(world_size.x): | |||
new_world.append([]) | |||
new_world[x].resize(world_size.x) | |||
for y in range(world_size.y): | |||
var pos = Vector2(x, y) | |||
if cell_is_alive(pos): | |||
total_living += 1 | |||
new_world[x][y] = create_cell(pos) | |||
else: | |||
new_world[x][y] = kill_cell(pos) | |||
world = new_world | |||
## Create the cell using the rendering server | |||
func create_cell(pos: Vector2) -> RID: | |||
if world[pos.x][pos.y] is RID: return world[pos.x][pos.y] | |||
var rs = RenderingServer | |||
cell_instance = rs.canvas_item_create() | |||
rs.canvas_item_set_parent(cell_instance, get_canvas_item()) | |||
var rect: Rect2 = Rect2(-cell_size.x/2, -cell_size.y/2, cell_size.x, cell_size.y) # Centered with cell size | |||
rs.canvas_item_add_texture_rect(cell_instance, rect, cell_texture) | |||
var pos_fixed = Vector2(pos.x * cell_size.x, pos.y * cell_size.y) | |||
var trans = Transform2D(0, pos_fixed) | |||
rs.canvas_item_set_transform(cell_instance, trans) | |||
return cell_instance | |||
func kill_cell(pos: Vector2) -> CellStates: | |||
if world[pos.x][pos.y] is RID: | |||
RenderingServer.free_rid(world[pos.x][pos.y]) | |||
return CellStates.DEAD | |||
## Generate the world with cells in random states (alive or dead) | |||
func generate_world() -> void: | |||
for x in range(world_size.x): | |||
world.append([]) | |||
world[x].resize(world_size.x) | |||
for y in range(world_size.y): | |||
if randi_range(0, 1): | |||
total_living += 1 | |||
world[x][y] = create_cell(Vector2(x, y)) | |||
else: | |||
world[x][y] = CellStates.DEAD | |||
func _on_generation_timer_timeout() -> void: | |||
process_generation() | |||
generation_counter.text = "Generation: %s" % generation | |||
living_cells_counter.text = "Living Cells: %s" % total_living | |||
generation_timer.start() |
@ -1,45 +0,0 @@ | |||
[gd_scene load_steps=4 format=3 uid="uid://d3twfk56sjf2m"] | |||
[ext_resource type="Script" path="res://world.gd" id="1_wavft"] | |||
[ext_resource type="Texture2D" uid="uid://c2vm5pfsamed4" path="res://icon.svg" id="2_8r6bn"] | |||
[ext_resource type="PackedScene" uid="uid://cy6vsgu8o0rad" path="res://scenes/fps_counter/fps_counter.tscn" id="3_ves6s"] | |||
[node name="World" type="Node2D"] | |||
position = Vector2(-152, 0) | |||
script = ExtResource("1_wavft") | |||
cell_texture = ExtResource("2_8r6bn") | |||
[node name="CanvasLayer" type="CanvasLayer" parent="."] | |||
[node name="Debug" type="MarginContainer" parent="CanvasLayer"] | |||
offset_right = 40.0 | |||
offset_bottom = 40.0 | |||
[node name="VBoxContainer" type="VBoxContainer" parent="CanvasLayer/Debug"] | |||
layout_mode = 2 | |||
[node name="FPSCounter" parent="CanvasLayer/Debug/VBoxContainer" instance=ExtResource("3_ves6s")] | |||
layout_mode = 2 | |||
text = "FPS: 0" | |||
[node name="WorldSeed" type="Label" parent="CanvasLayer/Debug/VBoxContainer"] | |||
layout_mode = 2 | |||
text = "World Seed: 0" | |||
[node name="GenerationCounter" type="Label" parent="CanvasLayer/Debug/VBoxContainer"] | |||
layout_mode = 2 | |||
text = "Generation: 1" | |||
[node name="LivingCellsCounter" type="Label" parent="CanvasLayer/Debug/VBoxContainer"] | |||
layout_mode = 2 | |||
text = "Living Cells: 0" | |||
[node name="Camera2D" type="Camera2D" parent="."] | |||
position = Vector2(464, 200) | |||
[node name="GenerationTimer" type="Timer" parent="."] | |||
wait_time = 0.05 | |||
one_shot = true | |||
autostart = true | |||
[connection signal="timeout" from="GenerationTimer" to="." method="_on_generation_timer_timeout"] |