@ -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"] |