Compare commits

...

6 Commits

Author SHA1 Message Date
Cat Flynn 2bc92d9580 add selection mode ui #5 2022-12-09 00:37:47 +00:00
Cat Flynn 989cd8f3c9 extract build mode to class #5 2022-12-09 00:37:47 +00:00
Cat Flynn 573eed58ab pulsing cursor #5 2022-12-09 00:37:47 +00:00
Cat Flynn 511ed61eda enter and exit build mode ui #5 2022-12-09 00:37:47 +00:00
Cat Flynn e988d9b38e change to build mode with keyboard shortcuts #5 2022-12-09 00:37:47 +00:00
ktyl 4650a5b7f3 diffusion (#12)
Reviewed-on: #12
2022-12-09 01:36:47 +01:00
23 changed files with 670 additions and 43 deletions

34
gdd.md
View File

@ -8,6 +8,19 @@
* Education
* Supply chains
## Interaction Modes
Interaction modes can be changed via keyboard shortcut.
### Selection Mode
This is the default interaction mode.
It can be accessed at any time by pressing the `esc` key.
### Build Mode
Build mode can be entered by pressing the `b` key.
## Critical Path
### Grid
@ -28,6 +41,27 @@ Add a tick counter as a UI element.
Diffusing should use 'pull' operations; tiles should pull values from surrounding tiles.
This is an attempt to think forwards to parallelising computation of diffusiong - ideally using the GPU.
#### Diffusion Algorithm
Diffusion is modelled as two-dimensional steady-state heat conduction.
This means that the amount of a value does not change between the start and end of the computation of the diffusion step.
The value at one 2D location is calculated based on the values of itself and its neighbours in the previous step.
A diffusion coefficient controls the rate of diffusion.
[Video explanation](https://www.youtube.com/watch?v=RGbV7T_iWT8)
[Derivation](https://www.tec-science.com/thermodynamics/heat/heat-equation-diffusion-equation/)
Implementation:
```cs
private float TransferHeat(float t0, float alpha, float nx, float ny, float px, float py)
{
float d2tdx2 = nx - 2 * t0 + px;
float d2tdy2 = ny - 2 * t0 + py;
return t0 + alpha * (d2tdx2 + d2tdy2);
}
```
### Tile Types
Tiles start as wild tiles.

View File

@ -4,6 +4,6 @@
[resource]
shader = ExtResource( 1 )
shader_param/baseColor = Color( 0.992157, 1, 0, 1 )
shader_param/highlightColor = Color( 1, 0, 0.984314, 1 )
shader_param/isHighlighted = 0
shader_param/lowColor = Color( 0, 0, 0, 0 )
shader_param/highColor = Color( 0, 0.976471, 1, 1 )
shader_param/t = null

View File

@ -5,6 +5,6 @@
[resource]
resource_local_to_scene = true
shader = ExtResource( 1 )
shader_param/baseColor = Color( 0.0823529, 0, 1, 1 )
shader_param/highlightColor = Color( 1, 0, 0, 1 )
shader_param/isHighlighted = null
shader_param/lowColor = Color( 0, 0, 0, 1 )
shader_param/highColor = Color( 1, 0.560784, 0, 1 )
shader_param/t = null

View File

@ -0,0 +1,7 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://scripts/Clock.cs" type="Script" id=1]
[node name="Clock" type="Node"]
script = ExtResource( 1 )
_tickRate = 10.0

View File

@ -1,11 +1,61 @@
[gd_scene load_steps=3 format=2]
[gd_scene load_steps=11 format=2]
[ext_resource path="res://nodes/grid.tscn" type="PackedScene" id=1]
[ext_resource path="res://nodes/grid_cursor.tscn" type="PackedScene" id=2]
[ext_resource path="res://nodes/ui/debug_ui.tscn" type="PackedScene" id=3]
[ext_resource path="res://nodes/clock.tscn" type="PackedScene" id=4]
[ext_resource path="res://nodes/interaction_modes/interaction_mode.tscn" type="PackedScene" id=5]
[ext_resource path="res://nodes/ui/build_mode_ui.tscn" type="PackedScene" id=6]
[ext_resource path="res://nodes/interaction_modes/build_mode.tscn" type="PackedScene" id=7]
[ext_resource path="res://nodes/interaction_modes/selection_mode.tscn" type="PackedScene" id=8]
[ext_resource path="res://nodes/ui/selection_mode_ui.tscn" type="PackedScene" id=9]
[node name="Root" type="Node2D"]
[sub_resource type="Curve" id=1]
_data = [ Vector2( 0, 0 ), 0.0, 5.0, 0, 0, Vector2( 0.5, 1 ), 0.0, 0.0, 0, 0, Vector2( 1, 0 ), -4.0, 0.0, 0, 0 ]
[node name="Root" type="Node"]
[node name="Clock" parent="." instance=ExtResource( 4 )]
_autoTick = true
[node name="Grid" parent="." instance=ExtResource( 1 )]
DiffusionCoefficient = 0.1
[node name="Cursor" parent="." instance=ExtResource( 2 )]
Grid = NodePath("../Grid")
_pulseShape = SubResource( 1 )
[node name="Interaction Mode" parent="." instance=ExtResource( 5 )]
_buildModePath = NodePath("../Build Mode")
_selectionModePath = NodePath("../Selection Mode")
[node name="Build Mode" parent="." instance=ExtResource( 7 )]
_key = 66
[node name="Selection Mode" parent="." instance=ExtResource( 8 )]
[node name="UI" type="Control" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
mouse_filter = 2
[node name="Debug" parent="UI" instance=ExtResource( 3 )]
[node name="Build Mode" parent="UI" instance=ExtResource( 6 )]
visible = false
_buildModePath = NodePath("../../Build Mode")
[node name="Selection Mode" parent="UI" instance=ExtResource( 9 )]
_selectionModePath = NodePath("../../Selection Mode")
[connection signal="OnPauseChanged" from="Clock" to="UI/Debug" method="_on_Clock_OnPauseChanged"]
[connection signal="OnTick" from="Clock" to="Grid" method="_on_Clock_OnTick"]
[connection signal="OnTick" from="Clock" to="UI/Debug" method="_on_Clock_OnTick"]
[connection signal="OnInteractionModeChanged" from="Interaction Mode" to="Cursor" method="_on_Interaction_Mode_OnInteractionModeChanged"]
[connection signal="OnInteractionModeChanged" from="Interaction Mode" to="UI/Debug" method="_on_Interaction_Mode_OnInteractionModeChanged"]
[connection signal="OnModeEntered" from="Build Mode" to="Interaction Mode" method="_on_Build_Mode_OnModeEntered"]
[connection signal="OnModeExited" from="Build Mode" to="Interaction Mode" method="_on_Build_Mode_OnModeExited"]
[connection signal="OnModeEntered" from="Selection Mode" to="Interaction Mode" method="_on_Selection_Mode_OnModeEntered"]
[connection signal="OnModeExited" from="Selection Mode" to="Interaction Mode" method="_on_Selection_Mode_OnModeExited"]
[connection signal="OnExit" from="UI/Build Mode" to="Interaction Mode" method="Reset"]
[connection signal="EnableBuildMode" from="UI/Selection Mode" to="Build Mode" method="Enable"]

View File

@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://scripts/interaction_modes/BuildMode.cs" type="Script" id=1]
[node name="Build Mode" type="Node"]
script = ExtResource( 1 )

View File

@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://scripts/interaction_modes/InteractionModes.cs" type="Script" id=1]
[node name="Interaction Mode" type="Node"]
script = ExtResource( 1 )

View File

@ -0,0 +1,7 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://scripts/interaction_modes/SelectionMode.cs" type="Script" id=1]
[node name="Selection Mode" type="Node"]
script = ExtResource( 1 )
_key = 16777217

View File

@ -0,0 +1,26 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://scripts/ui/BuildModeUI.cs" type="Script" id=1]
[node name="Build Mode" type="Control"]
anchor_top = 1.0
anchor_bottom = 1.0
margin_top = -600.0
margin_bottom = -600.0
script = ExtResource( 1 )
[node name="Exit" type="Button" parent="."]
margin_left = 2.0
margin_top = 562.0
margin_right = 38.0
margin_bottom = 598.0
text = "Exit"
[node name="Developed" type="Button" parent="."]
margin_left = 54.0
margin_top = 562.0
margin_right = 90.0
margin_bottom = 598.0
text = "Developed"
[connection signal="pressed" from="Exit" to="." method="_on_Exit_pressed"]

View File

@ -0,0 +1,48 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://scripts/ui/DebugUI.cs" type="Script" id=1]
[node name="Debug UI" type="Control"]
anchor_left = 1.0
anchor_right = 1.0
mouse_filter = 2
script = ExtResource( 1 )
TicksLabelPath = NodePath("Ticks")
PauseLabelPath = NodePath("Paused")
InteractionModeLabelPath = NodePath("Interaction Mode")
[node name="Interaction Mode" type="Label" parent="."]
anchor_left = 1.0
anchor_right = 1.0
margin_left = -102.0
margin_top = 15.0
margin_right = -14.0
margin_bottom = 29.0
grow_horizontal = 0
grow_vertical = 0
text = "mode: Selection"
align = 2
[node name="Ticks" type="Label" parent="."]
anchor_left = 1.0
anchor_right = 1.0
margin_left = -60.0
margin_top = 35.0
margin_right = -14.0
margin_bottom = 49.0
grow_horizontal = 0
grow_vertical = 0
text = "ticks: 0"
align = 2
[node name="Paused" type="Label" parent="."]
anchor_left = 1.0
anchor_right = 1.0
margin_left = -90.0
margin_top = 55.0
margin_right = -14.0
margin_bottom = 69.0
grow_horizontal = 0
grow_vertical = 0
text = "pause: false"
align = 2

View File

@ -0,0 +1,19 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://scripts/ui/SelectionModeUI.cs" type="Script" id=1]
[node name="Selection Mode UI" type="Control"]
anchor_top = 1.0
anchor_bottom = 1.0
margin_top = -600.0
margin_bottom = -600.0
script = ExtResource( 1 )
[node name="Build Mode" type="Button" parent="."]
margin_left = 2.0
margin_top = 562.0
margin_right = 38.0
margin_bottom = 598.0
text = "Build Mode"
[connection signal="pressed" from="Build Mode" to="." method="_on_Build_Mode_pressed"]

View File

@ -0,0 +1,67 @@
using Godot;
using System;
public class Clock : Node
{
[Signal]
delegate void OnTick(int ticks);
[Signal]
delegate void OnPauseChanged(bool paused);
[Export(PropertyHint.None, "Number of ticks per second")]
private float _tickRate;
private float TickPeriod => 1.0f / _tickRate;
[Export]
private bool _autoTick;
private int _ticks = 0;
private bool _paused = false;
public override void _Input(InputEvent @event)
{
base._Input(@event);
if (@event is InputEventKey keyEvent && keyEvent.Pressed)
{
switch ((KeyList)keyEvent.Scancode)
{
case KeyList.Space:
if (_autoTick)
{
_paused = !_paused;
EmitSignal(nameof(OnPauseChanged), _paused);
return;
}
Tick();
break;
}
}
}
private float _secondsSinceLastTick;
public override void _Process(float delta)
{
base._Process(delta);
if (!_autoTick)
return;
if (_paused)
return;
_secondsSinceLastTick += delta;
if (_secondsSinceLastTick < TickPeriod)
return;
_secondsSinceLastTick -= TickPeriod;
Tick();
}
private void Tick()
{
_ticks++;
EmitSignal(nameof(OnTick), _ticks);
}
}

View File

@ -8,7 +8,18 @@ public class GridCursor : Sprite
private WorldGrid _grid;
private ShaderMaterial _material;
private const string IS_HIGHLIGHTED = "isHighlighted";
#region Animation
[Export]
private Curve _pulseShape;
[Export]
private float _pulseRate = 1.0f;
private float PulsePeriod => 1.0f / _pulseRate;
#endregion
private float _elapsed = 0.0f;
private bool _pulsing = false;
private const string T = "t";
public override void _Ready()
{
@ -27,25 +38,44 @@ public class GridCursor : Sprite
this.Visible = _grid.IsInBounds(x, y);
var position = new Vector2(x + .5f, y + .5f) * _grid.CellSize;
this.Position = position;
return;
}
else if (@event is InputEventMouseButton mouseButtonEvent)
if (@event is InputEventMouseButton mouseButtonEvent)
{
switch ((ButtonList)mouseButtonEvent.ButtonIndex)
var button = (ButtonList)mouseButtonEvent.ButtonIndex;
if (button != ButtonList.Left)
return;
if (!mouseButtonEvent.Pressed)
{
case ButtonList.Left:
if (mouseButtonEvent.Pressed)
{
var pos = mouseButtonEvent.Position;
_grid.GetGridPos(pos, out var x, out var y);
_grid.ToggleTileHighlight(x, y);
_material.SetShaderParam(IS_HIGHLIGHTED, 1);
}
else
{
_material.SetShaderParam(IS_HIGHLIGHTED, 0);
}
break;
_material.SetShaderParam(T, 0f);
return;
}
var pos = mouseButtonEvent.Position;
_grid.GetGridPos(pos, out var x, out var y);
_grid.SetTileValue(x, y, 1.0f);
_material.SetShaderParam(T, 1f);
}
}
public override void _Process(float delta)
{
_elapsed += delta * _pulseRate;
if (!_pulsing)
{
_material.SetShaderParam(T, 1f);
return;
}
var a = _pulseShape.Interpolate(_elapsed % 1.0f);
_material.SetShaderParam(T, a);
}
public void _on_Interaction_Mode_OnInteractionModeChanged(Mode oldMode, Mode newMode)
{
_pulsing = newMode is BuildMode;
}
}

View File

@ -0,0 +1,8 @@
using Godot;
public struct Tile
{
public bool isHighlighted;
public float value;
public ShaderMaterial material;
}

View File

@ -12,12 +12,11 @@ public class WorldGrid : Node2D
[Export]
public int CellSize { get; set; }
private struct Tile
{
public bool isHighlighted;
public ShaderMaterial material;
}
[Export]
public float DiffusionCoefficient { get; set; } = 0.01f;
private Tile[,] _tiles;
private float[,] _nextValues = null;
// Called when the node enters the scene tree for the first time.
public override void _Ready()
@ -35,7 +34,7 @@ public class WorldGrid : Node2D
{
var tile = _tiles[x, y];
var material = tile.material;
material.SetShaderParam("isHighlighted", tile.isHighlighted ? 1 : 0);
material.SetShaderParam("t", tile.value);
}
}
}
@ -53,6 +52,14 @@ public class WorldGrid : Node2D
_tiles[x, y].isHighlighted ^= true;
}
public void SetTileValue(int x, int y, float value)
{
if (!IsInBounds(x, y))
return;
_tiles[x, y].value = value;
}
public void GetGridPos(Vector2 position, out int x, out int y)
{
x = Mathf.FloorToInt(position.x / CellSize);
@ -77,6 +84,7 @@ public class WorldGrid : Node2D
this.Clear();
_tiles = new Tile[size, size];
_nextValues = new float[size, size];
for (int x = 0; x < size; x++)
{
@ -84,6 +92,7 @@ public class WorldGrid : Node2D
{
var tile = new Tile();
tile.isHighlighted = false;
tile.value = 0.0f;
var node = TileScene.Instance<Node2D>();
this.AddChild(node);
@ -100,4 +109,58 @@ public class WorldGrid : Node2D
}
}
}
public void _on_Clock_OnTick(int ticks)
{
Diffuse();
}
private float TransferHeat(float t0, float alpha, float nx, float ny, float px, float py)
{
float d2tdx2 = nx - 2 * t0 + px;
float d2tdy2 = ny - 2 * t0 + py;
return t0 + alpha * (d2tdx2 + d2tdy2);
}
private void GetNeighbourTemperatures(int x, int y, out float nx, out float ny, out float px, out float py)
{
// default value
var t = _tiles[x, y].value;
nx = x > 0 ? _tiles[x - 1, y].value : t;
px = x < Size - 1 ? _tiles[x + 1, y].value : t;
ny = y > 0 ? _tiles[x, y - 1].value : t;
py = y < Size - 1 ? _tiles[x, y + 1].value : t;
}
private void Diffuse()
{
for (int x = 0; x < Size; x++)
{
for (int y = 0; y < Size; y++)
{
float t = _tiles[x, y].value;
var D = DiffusionCoefficient;
GetNeighbourTemperatures(
x, y,
out var nx,
out var ny,
out var px,
out var py);
// current value
_nextValues[x, y] = TransferHeat(t, D, nx, ny, px, py);
}
}
for (int x = 0; x < Size; x++)
{
for (int y = 0; y < Size; y++)
{
_tiles[x, y].value = _nextValues[x, y];
}
}
}
}

View File

@ -0,0 +1,9 @@
using Godot;
using System;
public class BuildMode : Mode
{
private const int NO_TILE_TYPE_SELECTED = -1;
public int SelectedTileType { get; set; } = NO_TILE_TYPE_SELECTED;
}

View File

@ -0,0 +1,75 @@
using Godot;
using System;
public class InteractionModes : Node
{
[Export]
private NodePath _buildModePath;
private BuildMode _buildMode;
[Export]
private NodePath _selectionModePath;
private SelectionMode _selectionMode;
[Signal]
delegate void OnInteractionModeChanged(Mode oldMode, Mode newMode);
private Mode _current = null;
public override void _Ready()
{
base._Ready();
_selectionMode = GetNode<SelectionMode>(_selectionModePath);
_buildMode = GetNode<BuildMode>(_buildModePath);
}
public override void _Process(float delta)
{
base._Process(delta);
if (_current == null)
{
Reset();
}
}
public void _on_Build_Mode_OnModeEntered()
{
_selectionMode.Disable();
ChangeMode(_selectionMode, _buildMode);
}
public void _on_Build_Mode_OnModeExited()
{
}
public void _on_Selection_Mode_OnModeEntered()
{
_buildMode.Disable();
ChangeMode(_buildMode, _selectionMode);
}
public void _on_Selection_Mode_OnModeExited()
{
}
private void ChangeMode(Mode oldMode, Mode newMode)
{
if (oldMode == newMode)
{
throw new InvalidOperationException();
}
if (_current != null && oldMode != _current)
{
throw new InvalidOperationException();
}
_current = newMode;
EmitSignal(nameof(OnInteractionModeChanged), oldMode, newMode);
}
public void Reset()
{
_buildMode.Disable();
_selectionMode.Enable();
}
}

View File

@ -0,0 +1,63 @@
using Godot;
using System;
public abstract class Mode : Node
{
[Signal]
delegate void OnModeEntered();
[Signal]
delegate void OnModeExited();
[Export]
private KeyList _key;
public bool Active
{
get => _active;
private set
{
if (value == _active)
return;
_active = value;
var signal = _active
? nameof(OnModeEntered)
: nameof(OnModeExited);
EmitSignal(signal);
}
}
private bool _active;
public override void _Input(InputEvent @event)
{
base._Input(@event);
if (!(@event is InputEventKey keyEvent))
return;
if (!keyEvent.Pressed)
return;
var key = (KeyList)keyEvent.Scancode;
if (key == _key)
{
Active = true;
}
}
public void Enable()
{
Active = true;
}
public void Disable()
{
Active = false;
}
public override string ToString()
{
return GetType().ToString();
}
}

View File

@ -0,0 +1,6 @@
using Godot;
using System;
public class SelectionMode : Mode
{
}

View File

@ -0,0 +1,36 @@
using Godot;
using System;
public class BuildModeUI : Control
{
[Signal]
delegate void OnExit();
[Export]
private NodePath _buildModePath;
private BuildMode _buildMode;
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
_buildMode = GetNode<BuildMode>(_buildModePath);
Visible = false;
}
public void _on_Exit_pressed()
{
Exit();
}
private void Exit()
{
EmitSignal(nameof(OnExit));
}
public override void _Process(float delta)
{
base._Process(delta);
Visible = _buildMode.Active;
}
}

View File

@ -0,0 +1,41 @@
using Godot;
using System;
public class DebugUI : Control
{
[Export]
public NodePath TicksLabelPath { private get; set; }
private Label _ticksLabel;
[Export]
public NodePath PauseLabelPath { private get; set; }
private Label _pauseLabel;
[Export]
public NodePath InteractionModeLabelPath { private get; set; }
private Label _interactionModeLabel;
public override void _Ready()
{
_ticksLabel = GetNode<Label>(TicksLabelPath);
_pauseLabel = GetNode<Label>(PauseLabelPath);
_interactionModeLabel = GetNode<Label>(InteractionModeLabelPath);
}
#region Signals
public void _on_Clock_OnPauseChanged(bool paused) =>
SetLabelText(_pauseLabel, "pause", paused);
public void _on_Clock_OnTick(int ticks) =>
SetLabelText(_ticksLabel, "ticks", ticks);
public void _on_Interaction_Mode_OnInteractionModeChanged(Mode oldMode, Mode newMode)
{
SetLabelText(_interactionModeLabel, "mode", newMode);
}
#endregion
private void SetLabelText(Label label, string key, object value)
{
label.Text = $"{key}: {value}";
}
}

View File

@ -0,0 +1,26 @@
using Godot;
using System;
public class SelectionModeUI : Control
{
[Signal]
delegate void EnableBuildMode();
[Export]
private NodePath _selectionModePath;
public SelectionMode _selectionMode;
public void _on_Build_Mode_pressed() => EmitSignal(nameof(EnableBuildMode));
public override void _Ready()
{
_selectionMode = GetNode<SelectionMode>(_selectionModePath);
}
public override void _Process(float delta)
{
base._Process(delta);
Visible = _selectionMode.Active;
}
}

View File

@ -1,9 +1,9 @@
shader_type canvas_item;
uniform vec4 baseColor : hint_color;
uniform vec4 highlightColor : hint_color;
uniform int isHighlighted;
uniform vec4 lowColor : hint_color;
uniform vec4 highColor : hint_color;
uniform float t;
void fragment() {
COLOR = mix(baseColor, highlightColor, float(isHighlighted));
COLOR = mix(lowColor, highColor, t);
}