27 Commits
v1.1 ... master

Author SHA1 Message Date
  Ryan Reed a8bdc15c71 Cleanup and renaming of the camera script 1 year ago
  Ryan Reed a5f6310b16 Removing unnecessary mouse reference 1 year ago
  Ryan Reed 8372311432 Fixing input names 1 year ago
  Ryan Reed ebece003eb Allowing for camera movement and moving camera functions out of conway.gd 1 year ago
  Ryan Reed ad12c996a3 Minor cleanup 1 year ago
  Ryan Reed d1cdb4c186 Changing directory structure and cleanup 1 year ago
  Ryan Reed 3cb0d0e7ae Removing Cell Size inputs and adding Generation Speed 1 year ago
  Ryan Reed b7d46e2567 Added detection of stalled evolutions 1 year ago
  Ryan Reed 0d39ee63ee Adding ability to reset zoom 1 year ago
  Ryan Reed 71e110dac9 Cleaing the world before generation 1 year ago
  Ryan Reed ecd2b9f952 Replacign range with CellStates for legibility 1 year ago
  Ryan Reed f41795e6b4 Adding ability to zoom 1 year ago
  Ryan Reed 9ae678d8f6 Minor cleanup 1 year ago
  Ryan Reed df315da676 Fixing toggle_pause timer 1 year ago
  Ryan Reed 41cc8ea214 Minor cleanup 1 year ago
  Ryan Reed f46abc9df4 Minor cleanup 1 year ago
  Ryan Reed 909a831c60 Adding a Next Generation option and reworking how pausing is toggled 1 year ago
  Ryan Reed 47cade0dbc Moving pause check for clarity 1 year ago
  Ryan Reed 9bf71faa4a Merge pull request 'Rework RenderingServer use' (#1) from rendering-rework into master 1 year ago
  Ryan Reed 533f3ad38b Hide cells instead of creating new instances on every generation 1 year ago
  Ryan Reed a6016240a9 Fixing Cell Size Y input 1 year ago
  Ryan Reed e1b3c525e4 Fixing Spelling 1 year ago
  Ryan Reed ff25603b1c Adding Quit button and cleaning up UI layout 1 year ago
  Ryan Reed 54c60ec813 Adding Select on Focus to inputs 1 year ago
  Ryan Reed db175a6912 Adding missing import 1 year ago
  Ryan Reed e938ec33d7 Adding missing image 1 year ago
  Ryan Reed 44f9b025d5 Adding a generate button to the UI 1 year ago
12 changed files with 491 additions and 268 deletions
Split View
  1. +2
    -1
      README.md
  2. +0
    -0
      assets/icon.svg
  3. +3
    -3
      assets/icon.svg.import
  4. BIN
      assets/refresh.png
  5. +34
    -0
      assets/refresh.png.import
  6. +49
    -2
      project.godot
  7. +21
    -0
      scenes/camera.gd
  8. +308
    -0
      scenes/conway.gd
  9. +74
    -32
      scenes/conway.tscn
  10. +0
    -10
      scenes/fps_counter/fps_counter.gd
  11. +0
    -8
      scenes/fps_counter/fps_counter.tscn
  12. +0
    -212
      world.gd

+ 2
- 1
README.md View File

@ -2,5 +2,6 @@
My attempt at a [Conway's Game of Life](https://en.wikipedia.org/wiki/Conway's_Game_of_Life) simulation using Godot 4
The project is also utilizing a RenderingServer to generate the cells.
The project is also utilizing a RenderingServer to generate the cells. This might be overkill but though it would be fun to try.
The project is over engineered but what isn't?

icon.svg → assets/icon.svg View File


icon.svg.import → assets/icon.svg.import View File

@ -3,15 +3,15 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://c2vm5pfsamed4"
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
path="res://.godot/imported/icon.svg-56083ea2a1f1a4f1e49773bdc6d7826c.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://icon.svg"
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
source_file="res://assets/icon.svg"
dest_files=["res://.godot/imported/icon.svg-56083ea2a1f1a4f1e49773bdc6d7826c.ctex"]
[params]

BIN
assets/refresh.png View File

Before After
Width: 26  |  Height: 26  |  Size: 322 B

+ 34
- 0
assets/refresh.png.import View File

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

+ 49
- 2
project.godot View File

@ -11,9 +11,9 @@ config_version=5
[application]
config/name="Conway's Game of Life"
run/main_scene="res://world.tscn"
run/main_scene="res://scenes/conway.tscn"
config/features=PackedStringArray("4.1", "Forward Plus")
config/icon="res://icon.svg"
config/icon="res://assets/icon.svg"
[input]
@ -28,3 +28,50 @@ Menu={
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194305,"key_label":0,"unicode":0,"echo":false,"script":null)
]
}
"Next Generation"={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194321,"key_label":0,"unicode":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":78,"key_label":0,"unicode":110,"echo":false,"script":null)
]
}
"Zoom In"={
"deadzone": 0.5,
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":8,"position":Vector2(178, 5),"global_position":Vector2(182, 48),"factor":1.0,"button_index":4,"canceled":false,"pressed":true,"double_click":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":61,"key_label":0,"unicode":61,"echo":false,"script":null)
]
}
"Zoom Out"={
"deadzone": 0.5,
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":16,"position":Vector2(231, 17),"global_position":Vector2(235, 60),"factor":1.0,"button_index":5,"canceled":false,"pressed":true,"double_click":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":45,"key_label":0,"unicode":45,"echo":false,"script":null)
]
}
"Reset Zoom"={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":82,"key_label":0,"unicode":114,"echo":false,"script":null)
]
}
Left={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194319,"key_label":0,"unicode":0,"echo":false,"script":null)
]
}
Right={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194321,"key_label":0,"unicode":0,"echo":false,"script":null)
]
}
Up={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194320,"key_label":0,"unicode":0,"echo":false,"script":null)
]
}
Down={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194322,"key_label":0,"unicode":0,"echo":false,"script":null)
]
}

+ 21
- 0
scenes/camera.gd View File

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

+ 308
- 0
scenes/conway.gd View File

@ -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()

world.tscn → scenes/conway.tscn View File

@ -1,16 +1,18 @@
[gd_scene load_steps=4 format=3 uid="uid://d3twfk56sjf2m"]
[gd_scene load_steps=5 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"]
[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_wavft")
cell_texture = ExtResource("2_8r6bn")
script = ExtResource("1_1gvb0")
cell_texture = ExtResource("2_l6fa6")
[node name="Camera2D" type="Camera2D" parent="."]
[node name="Camera" type="Camera2D" parent="."]
position = Vector2(464, 200)
script = ExtResource("3_e0v0d")
[node name="GenerationTimer" type="Timer" parent="."]
wait_time = 0.05
@ -43,10 +45,6 @@ offset_bottom = 116.0
[node name="VBoxContainer" type="VBoxContainer" parent="UI/Debug"]
layout_mode = 2
[node name="FPSCounter" parent="UI/Debug/VBoxContainer" instance=ExtResource("3_ves6s")]
layout_mode = 2
text = "FPS: 0"
[node name="WorldSeed" type="Label" parent="UI/Debug/VBoxContainer"]
layout_mode = 2
text = "World Seed: 0"
@ -90,10 +88,17 @@ 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
@ -104,6 +109,11 @@ 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:"
@ -113,6 +123,7 @@ 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
@ -123,42 +134,73 @@ custom_minimum_size = Vector2(20, 0)
layout_mode = 2
text = "32"
max_length = 4
select_all_on_focus = true
[node name="CellSize" type="HBoxContainer" parent="UI/WorldGeneration/VBoxContainer"]
[node name="GenerationSpeed" type="HBoxContainer" parent="UI/WorldGeneration/VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 6
[node name="Label" type="Label" parent="UI/WorldGeneration/VBoxContainer/CellSize"]
custom_minimum_size = Vector2(120, 0)
[node name="Label" type="Label" parent="UI/WorldGeneration/VBoxContainer/GenerationSpeed"]
layout_mode = 2
text = "Cell Size"
text = "Generation Speed"
[node name="Label_x" type="Label" parent="UI/WorldGeneration/VBoxContainer/CellSize"]
[node name="Padding" type="VSeparator" parent="UI/WorldGeneration/VBoxContainer/GenerationSpeed"]
modulate = Color(1, 1, 1, 0)
layout_mode = 2
text = "X:"
size_flags_horizontal = 3
[node name="Input_x" type="LineEdit" parent="UI/WorldGeneration/VBoxContainer/CellSize"]
custom_minimum_size = Vector2(20, 0)
[node name="Input" type="HSlider" parent="UI/WorldGeneration/VBoxContainer/GenerationSpeed"]
custom_minimum_size = Vector2(200, 0)
layout_mode = 2
text = "16"
max_length = 4
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="Label_y" type="Label" parent="UI/WorldGeneration/VBoxContainer/CellSize"]
[node name="FinalRow" type="HBoxContainer" parent="UI/WorldGeneration/VBoxContainer"]
layout_mode = 2
text = "Y:"
[node name="Input_y" type="LineEdit" parent="UI/WorldGeneration/VBoxContainer/CellSize"]
custom_minimum_size = Vector2(20, 0)
[node name="StartPausedButton" type="CheckBox" parent="UI/WorldGeneration/VBoxContainer/FinalRow"]
layout_mode = 2
text = "16"
max_length = 4
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="Submit" type="HBoxContainer" parent="UI/WorldGeneration/VBoxContainer"]
[node name="QuitButton" type="Button" parent="UI/WorldGeneration/VBoxContainer/FinalRow"]
layout_direction = 3
layout_mode = 2
text = "Quit"
[node name="Button" type="Button" parent="UI/WorldGeneration/VBoxContainer/Submit"]
[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
text = "Start"
[connection signal="timeout" from="GenerationTimer" to="." method="_on_generation_timer_timeout"]
[connection signal="pressed" from="UI/WorldGeneration/VBoxContainer/Submit/Button" to="." method="_on_generation_submit_pressed"]
[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"]

+ 0
- 10
scenes/fps_counter/fps_counter.gd View File

@ -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())

+ 0
- 8
scenes/fps_counter/fps_counter.tscn View File

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

+ 0
- 212
world.gd View File

@ -1,212 +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 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_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 gneration_cell_size_x: LineEdit = $UI/WorldGeneration/VBoxContainer/CellSize/Input_x
@onready var gneration_cell_size_y: LineEdit = $UI/WorldGeneration/VBoxContainer/CellSize/Input_y
## Other
@onready var camera: Camera2D = $Camera2D
@onready var generation_timer: Timer = $GenerationTimer
@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 is_paused: int = false
var total_living: int = 0
var world: Array = []
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"):
is_paused = !is_paused
background_ui.visible = is_paused
if event.is_action_pressed("Menu"):
is_paused = true
background_ui.visible = true
generation_ui.visible = true
update_generation_ui()
#
# UI
#
## Update the Gneration 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)
gneration_cell_size_x.text = str(cell_size.x)
gneration_cell_size_x.text = str(cell_size.y)
#
# Conway Specific
#
func start_conway() -> void:
debug_world_seed.text = "World Seeed: %s" % world_seed
is_paused = false
generate_world()
generation_timer.start()
# 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
## 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 = 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 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 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:
if is_paused: return
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
## Remove the cell from the RenderingServer, if it exists
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()
debug_generation_counter.text = "Generation: %s" % generation
debug_living_cells_counter.text = "Living Cells: %s" % total_living
generation_timer.start()
func _on_generation_submit_pressed() -> void:
if not generation_seed.text.strip_edges():
world_seed = randi()
else:
world_seed = int(generation_seed.text)
world_size = Vector2(int(generation_world_size_x.text), int(generation_world_size_y.text))
cell_size = Vector2(int(gneration_cell_size_x.text), int(gneration_cell_size_x.text))
debug_ui.visible = true
generation_ui.visible = false
background_ui.visible = false
seed(world_seed)
start_conway()

Loading…
Cancel
Save