diff --git a/FOU.csproj b/FOU.csproj index 088aa3d..70ab8a3 100644 --- a/FOU.csproj +++ b/FOU.csproj @@ -1,6 +1,9 @@ - + - net6.0 + net8.0 true + + + \ No newline at end of file diff --git a/Scenes/FPSLabel.cs b/Scenes/FPSLabel.cs index a204847..55f8f8d 100644 --- a/Scenes/FPSLabel.cs +++ b/Scenes/FPSLabel.cs @@ -2,7 +2,8 @@ using Godot; public partial class FPSLabel : Label { - bool ShowFPS = true; + [Export] + public bool ShowFPS = true; // Called every frame. 'delta' is the elapsed time since the previous frame. public override void _Process(double delta) { diff --git a/Scenes/FPSLabel.cs.uid b/Scenes/FPSLabel.cs.uid new file mode 100644 index 0000000..7ff74b6 --- /dev/null +++ b/Scenes/FPSLabel.cs.uid @@ -0,0 +1 @@ +uid://d3cbk8f7lckbr diff --git a/Scenes/PerfDetails.cs b/Scenes/PerfDetails.cs new file mode 100644 index 0000000..219ee7c --- /dev/null +++ b/Scenes/PerfDetails.cs @@ -0,0 +1,20 @@ +using FOU.Scripts; +using Godot; + +public partial class PerfDetails : Label +{ + [Export] + public bool ShowDetails = true; + + public override void _Process(double delta) + { + Text = ""; + if (!ShowDetails) return; + + int activeElements = 0; + foreach (Chunk c in Main.Instance.Level.GetChunks()) + activeElements += c.ActiveElementsCount(); + + Text += $"Active Elements: {activeElements}"; + } +} diff --git a/Scenes/PerfDetails.cs.uid b/Scenes/PerfDetails.cs.uid new file mode 100644 index 0000000..7171816 --- /dev/null +++ b/Scenes/PerfDetails.cs.uid @@ -0,0 +1 @@ +uid://52grhydeyy0t diff --git a/Scenes/SettingsController.cs.uid b/Scenes/SettingsController.cs.uid new file mode 100644 index 0000000..6f68b87 --- /dev/null +++ b/Scenes/SettingsController.cs.uid @@ -0,0 +1 @@ +uid://dxmkbb5f1t368 diff --git a/Scenes/main.tscn b/Scenes/main.tscn index 9b78cab..187b0df 100644 --- a/Scenes/main.tscn +++ b/Scenes/main.tscn @@ -1,14 +1,15 @@ -[gd_scene load_steps=4 format=3 uid="uid://cf34vk5r055dx"] +[gd_scene load_steps=5 format=3 uid="uid://cf34vk5r055dx"] -[ext_resource type="Script" path="res://Scripts/Main.cs" id="1_k1i8e"] -[ext_resource type="Script" path="res://Scenes/FPSLabel.cs" id="2_8cb7y"] -[ext_resource type="Script" path="res://Scenes/SettingsController.cs" id="3_a4w6m"] +[ext_resource type="Script" uid="uid://cmvvubxfvdca7" path="res://Scripts/Main.cs" id="1_k1i8e"] +[ext_resource type="Script" uid="uid://d3cbk8f7lckbr" path="res://Scenes/FPSLabel.cs" id="2_8cb7y"] +[ext_resource type="Script" uid="uid://dxmkbb5f1t368" path="res://Scenes/SettingsController.cs" id="3_a4w6m"] +[ext_resource type="Script" uid="uid://52grhydeyy0t" path="res://Scenes/PerfDetails.cs" id="3_u2p48"] [node name="Main" type="Node2D"] script = ExtResource("1_k1i8e") DebugVisualization = true BrushSize = 2 -TextureResolution = 0.35 +RainAmount = 1.0 [node name="CanvasLayer" type="CanvasLayer" parent="."] @@ -34,6 +35,15 @@ text = "FPS: 0 " script = ExtResource("2_8cb7y") +[node name="PerfDetails" type="Label" parent="CanvasLayer/TopUI"] +layout_mode = 0 +offset_top = 30.0 +offset_right = 76.0 +offset_bottom = 67.0 +theme_override_font_sizes/font_size = 10 +text = "Active Elements: 0" +script = ExtResource("3_u2p48") + [node name="Settings" type="VBoxContainer" parent="CanvasLayer/TopUI"] layout_mode = 1 anchors_preset = 1 diff --git a/Scripts/Chunk.cs b/Scripts/Chunk.cs index b178496..ece2b90 100644 --- a/Scripts/Chunk.cs +++ b/Scripts/Chunk.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using FOU.Scripts.Elements; using Godot; @@ -15,6 +16,9 @@ public class Chunk { private readonly Image image; private readonly int sizeX; private readonly int sizeY; + private readonly HashSet activeElements; + private readonly HashSet toAddElements = new(); + private readonly HashSet toRemoveElements = new(); public Chunk(int x, int y, int index) { Index = index; @@ -28,43 +32,62 @@ public class Chunk { } } - image = Image.Create(sizeX, sizeY, false, Image.Format.Rgb8); + activeElements = new HashSet(sizeX * sizeY); + + image = Image.CreateEmpty(sizeX, sizeY, false, Image.Format.Rgb8); image.Fill(Colors.Black); } public void Update() { - for (int x = 0; x < sizeX; x++) { - for (int y = 0; y < sizeY; y++) { - if (Elements[x,y] != null) - Elements[x,y].Update(); + foreach (Element e in activeElements) + e.Update(); + + // apply changes: + if (toRemoveElements.Count > 0) { + foreach (Element removeElement in toRemoveElements) { + activeElements.Remove(removeElement); + removeElement.ResetColor(); } + toRemoveElements.Clear(); + } + + if (toAddElements.Count > 0) { + foreach (Element addElement in toAddElements) { + activeElements.Add(addElement); + addElement.SetDebugColor(Colors.Green); + } + toAddElements.Clear(); } } - public void WritePixel(int x, int y, int size) { - int halfsize = size/2; + public void SetElementActive(Element e, bool active) { + if (e.GetType() == typeof(Element) && active) return; + + if (active) AddToChunk(e); + else RemoveFromChunk(e); + } + + public void WritePixel(int x, int y) { + if (x < 0 || x >= sizeX || y < 0 || y >= sizeY) { + GD.PrintErr($"Out of bounds for chunk: {x}:{y}"); + return; + } Type type = typeof(T); + object o = Activator.CreateInstance(type, x, y, this); - for (int i = -halfsize; i <= halfsize; i++) { - for (int j = -halfsize; j <= halfsize; j++) { - int X = Mathf.Clamp(x + i, 0, sizeX-1); - int Y = Mathf.Clamp(y + j, 0, sizeY-1); - object o = Activator.CreateInstance(type, X, Y, this); - - if (Elements[X,Y].GetType() != type) - Elements[X,Y] = o as Element; - } + // check if not empty: + if (Elements[x,y].GetType() != typeof(Element)) { + return; } + + Elements[x, y].Chunk.RemoveFromChunk(Elements[x, y]); + Elements[x, y] = o as Element; + Elements[x, y].Active = true; } - public Image DrawLevel() { - for (int x = 0; x < sizeX; x++) { - for (int y = 0; y < sizeY; y++) { - image.SetPixel(x,y, Elements[x,y].Color); - } - } - return image; + public int ActiveElementsCount() { + return activeElements.Count; } public void Swap(Element what, Vector2I pos) { @@ -111,6 +134,12 @@ public class Chunk { GD.PrintErr("Trying to swap null"); return; } + + if (what.Chunk != swapTo.Chunk) { + what.Chunk.RemoveFromChunk(what); + swapTo.Chunk.AddToChunk(what); + } + Element temp = new Element(what); what.Position = swapTo.Position; @@ -122,6 +151,20 @@ public class Chunk { swapTo.Chunk.Elements[swapTo.Position.X, swapTo.Position.Y] = swapTo; what.Active = true; + what.Moved(); swapTo.Active = true; + swapTo.Moved(); + } + + public override string ToString() { + return $"Chunk {Index}"; + } + + private void RemoveFromChunk(Element e) { + toRemoveElements.Add(e); + } + + private void AddToChunk(Element e) { + toAddElements.Add(e); } } diff --git a/Scripts/Chunk.cs.uid b/Scripts/Chunk.cs.uid new file mode 100644 index 0000000..76de819 --- /dev/null +++ b/Scripts/Chunk.cs.uid @@ -0,0 +1 @@ +uid://r17upkbkmfiy diff --git a/Scripts/Elements/Dirt.cs b/Scripts/Elements/Dirt.cs index e55b14e..f6e5653 100644 --- a/Scripts/Elements/Dirt.cs +++ b/Scripts/Elements/Dirt.cs @@ -5,7 +5,7 @@ namespace FOU.Scripts.Elements; public class Dirt : Solid { public Dirt(int x, int y, ref Chunk chunk) : base(x, y, ref chunk) { - Color = AddColorVariance(Colors.Brown); + color = AddColorVariance(Colors.Brown); Density = 10; DiffuseSpeed = 50; } diff --git a/Scripts/Elements/Dirt.cs.uid b/Scripts/Elements/Dirt.cs.uid new file mode 100644 index 0000000..4e6a188 --- /dev/null +++ b/Scripts/Elements/Dirt.cs.uid @@ -0,0 +1 @@ +uid://80esqy0cardd diff --git a/Scripts/Elements/Element.cs b/Scripts/Elements/Element.cs index 3b9185e..62fe24c 100644 --- a/Scripts/Elements/Element.cs +++ b/Scripts/Elements/Element.cs @@ -3,49 +3,51 @@ namespace FOU.Scripts.Elements; public class Element { - public Color Color = Colors.Black; - public Vector2I Position; public Chunk Chunk; - public int Density; - public int LastUpdate = -1; - public int LastMove = 0; + public Vector2I Position; - public int DiffuseSpeed = 10; - public const int MAX_DIFFUSE_SPEED = 100; - public const int STEPS_UNTIL_INACTIVE = 500; + protected int Density; + protected int DiffuseSpeed = 10; + protected Color color = Colors.Black; + protected const int MAX_DIFFUSE_SPEED = 100; + protected const int STEPS_UNTIL_INACTIVE = 500; protected const float MAX_COLOR_VARIANCE = 0.1f; protected static readonly Vector2I VERTICAL_OPPOSITE = new Vector2I(-1, 1); + protected int lastMove = 0; - private bool active = true; + private bool active = false; + private int lastUpdate = -1; private Color originalColor; public Element(Element e) { Position = e.Position; Chunk = e.Chunk; - LastMove = Engine.GetFramesDrawn(); + lastMove = Engine.GetFramesDrawn(); } public Element(int x, int y, Chunk chunk) { Position.X = x; Position.Y = y; Chunk = chunk; - LastMove = Engine.GetFramesDrawn(); + lastMove = Engine.GetFramesDrawn(); + Active = false; } public bool Active { get => active; set { - active = value; + if (active == value) return; - if (active) - Color = originalColor; - else if (Main.Instance.DebugVisualization) - Color = new Color(0.2f, 0.2f, 0.2f); + active = value; + Chunk.SetElementActive(this, value); + // SetDebugColor(value, new Color(0.2f, 0.2f, 0.2f)); } } + public Color Color => color; + /// /// base update method, checks if anything is to do at all /// @@ -54,14 +56,14 @@ public class Element { public virtual bool Update() { if (!Active) return false; - if (LastUpdate == Engine.GetFramesDrawn()) return false; // already updated this frame - LastUpdate = Engine.GetFramesDrawn(); + if (lastUpdate == Engine.GetFramesDrawn()) return false; // already updated this frame + lastUpdate = Engine.GetFramesDrawn(); return true; } public override string ToString() { - return $"{GetType()} {Position}[{Chunk.Index}]"; + return $"{GetType()} {Position}[{Chunk.Index}] {active}"; } protected virtual void Tick() { @@ -99,4 +101,17 @@ public class Element { return c; } + public void ResetColor() { + color = originalColor; + } + + public void SetDebugColor(Color color) { + if (!Main.Instance.DebugVisualization) return; + + this.color = color; + } + + public void Moved() { + lastMove = Engine.GetFramesDrawn(); + } } diff --git a/Scripts/Elements/Element.cs.uid b/Scripts/Elements/Element.cs.uid new file mode 100644 index 0000000..1735331 --- /dev/null +++ b/Scripts/Elements/Element.cs.uid @@ -0,0 +1 @@ +uid://b5lme1c4rx15n diff --git a/Scripts/Elements/Liquid.cs b/Scripts/Elements/Liquid.cs index 1d40ae6..ef1fe19 100644 --- a/Scripts/Elements/Liquid.cs +++ b/Scripts/Elements/Liquid.cs @@ -9,7 +9,7 @@ public abstract class Liquid : Element { public override bool Update() { if (!base.Update()) return false; - if (LastMove + STEPS_UNTIL_INACTIVE < Engine.GetFramesDrawn()) Active = false; + if (lastMove + STEPS_UNTIL_INACTIVE < Engine.GetFramesDrawn()) Active = false; Tick(); diff --git a/Scripts/Elements/Liquid.cs.uid b/Scripts/Elements/Liquid.cs.uid new file mode 100644 index 0000000..a3d57e6 --- /dev/null +++ b/Scripts/Elements/Liquid.cs.uid @@ -0,0 +1 @@ +uid://bqx36wisy7127 diff --git a/Scripts/Elements/Solid.cs b/Scripts/Elements/Solid.cs index 0257ed4..a9803ca 100644 --- a/Scripts/Elements/Solid.cs +++ b/Scripts/Elements/Solid.cs @@ -7,7 +7,7 @@ public abstract class Solid : Element { public override bool Update() { if (!base.Update()) return false; - if (LastMove + STEPS_UNTIL_INACTIVE < Engine.GetFramesDrawn()) Active = false; + if (lastMove + STEPS_UNTIL_INACTIVE < Engine.GetFramesDrawn()) Active = false; Tick(); diff --git a/Scripts/Elements/Solid.cs.uid b/Scripts/Elements/Solid.cs.uid new file mode 100644 index 0000000..5fe51ca --- /dev/null +++ b/Scripts/Elements/Solid.cs.uid @@ -0,0 +1 @@ +uid://ur56t06r7n4l diff --git a/Scripts/Elements/Water.cs b/Scripts/Elements/Water.cs index 200f2b5..aeb93b8 100644 --- a/Scripts/Elements/Water.cs +++ b/Scripts/Elements/Water.cs @@ -5,7 +5,7 @@ namespace FOU.Scripts.Elements; public class Water : Liquid { public Water(int x, int y, ref Chunk chunk) : base(x, y, ref chunk) { - Color = AddColorVariance(Colors.Blue); + color = AddColorVariance(Colors.Blue); Density = 1; } diff --git a/Scripts/Elements/Water.cs.uid b/Scripts/Elements/Water.cs.uid new file mode 100644 index 0000000..f167cfc --- /dev/null +++ b/Scripts/Elements/Water.cs.uid @@ -0,0 +1 @@ +uid://k8id5egjc7x8 diff --git a/Scripts/Level.cs b/Scripts/Level.cs index a471dc1..e39de58 100644 --- a/Scripts/Level.cs +++ b/Scripts/Level.cs @@ -11,46 +11,45 @@ public class Level { private Chunk[,] chunks; private Image image; - private int chunksX; - private int chunksY; + private int chunksPerX; + private int chunksPerY; // per chunk: private int chunkResX; private int chunkResY; - private bool enableRain = false; private float rainAmount = 0; public Level(Main main, int sizeX, int sizeY) { - GD.Print($"Generating level ({sizeX}:{sizeY}) with {chunksX*chunksY} chunks ({chunkResX} * {chunkResY})"); - Resolution = new Vector2I(sizeX, sizeY); - chunksX = main.ChunksPerAxis; - chunksY = main.ChunksPerAxis; + chunksPerX = main.ChunksPerAxis; + chunksPerY = main.ChunksPerAxis; - chunkResX = sizeX / chunksX; - chunkResY = sizeY / chunksY; + chunkResX = sizeX / chunksPerX; + chunkResY = sizeY / chunksPerY; + + GD.Print($"Generating level ({sizeX}:{sizeY}) with {chunksPerX*chunksPerY} chunks ({chunkResX} * {chunkResY})"); image = Image.Create(sizeX, sizeY, false, Image.Format.Rgb8); - chunks = new Chunk[chunksX, chunksY]; + chunks = new Chunk[chunksPerX, chunksPerY]; int index = 0; // create all chunks - for (int x = 0; x < chunksX; x++) { - for (int y = 0; y < chunksY; y++) { + for (int x = 0; x < chunksPerX; x++) { + for (int y = 0; y < chunksPerY; y++) { chunks[x, y] = new Chunk(chunkResX, chunkResY, index++); } } // assign neighbors - for (int x = 0; x < chunksX; x++) { - for (int y = 0; y < chunksY; y++) { + for (int x = 0; x < chunksPerX; x++) { + for (int y = 0; y < chunksPerY; y++) { if (y > 0) chunks[x, y].NeighborN = chunks[x, y-1]; - if (y < chunksY-1) chunks[x, y].NeighborS = chunks[x, y+1]; + if (y < chunksPerY-1) chunks[x, y].NeighborS = chunks[x, y+1]; if (x > 0) chunks[x, y].NeighborE = chunks[x-1, y]; - if (x < chunksX-1) chunks[x, y].NeighborW = chunks[x+1, y]; + if (x < chunksPerX-1) chunks[x, y].NeighborW = chunks[x+1, y]; } } } @@ -66,6 +65,19 @@ public class Level { rainAmount = amount; } + public void StartBenchmark() { + GD.Print("benchmark"); + for (int x = 0; x < chunksPerX; x++) { + for (int y = 0; y < chunksPerY; y++) { + WritePixel(x * chunkResX/2, y * chunkResY/2, 100); + } + } + } + + public Chunk[,] GetChunks() { + return chunks; + } + private void MakeItRain() { if (rainAmount < .1f) return; @@ -73,20 +85,70 @@ public class Level { for (int i = 0; i <= rainDrops; i++) { if (GD.Randf() < rainAmount) - WritePixel((int)(GD.Randi() % (chunkResX * chunksX)), 0, 1); + WritePixel((int)(GD.Randi() % (chunkResX * chunksPerX)), 0, 1); } } public void WritePixel(int x, int y, int size) { - chunks[x/chunkResX, y/chunkResY].WritePixel(x % chunkResX, y % chunkResY, size); + int halfsize = size/2; + + for (int i = -halfsize; i <= halfsize; i++) { + for (int j = 0; j < 1; j++) { + // for (int j = -halfsize; j <= halfsize; j++) { + // calculate in-chunk coordinates + int inChunkX = x % chunkResX; + int inChunkY = y % chunkResY; + int brushX = i; + int brushY = j; + + // calculate chunk to write to + int chunkX = x/chunkResX; + if (inChunkX + i < 0) { + chunkX--; + // 320 -22 + brushX = chunkResX + inChunkX + i; + inChunkX = chunkResX - inChunkX; + } else if (inChunkX + i >= chunkResX) { + chunkX++; + inChunkX = 0; + brushX = i % chunkResX; + } + inChunkX += brushX; + + if (chunkX < 0 || chunkX >= chunksPerX) { + GD.PrintErr($"Trying to write out of bounds: {x}:{y}"); + return; + } + + int chunkY = y/chunkResY; + if (inChunkY + j < 0) { + chunkY--; + brushY = chunkResY - (inChunkY + j); + inChunkY = chunkResY - inChunkY; + } else if (inChunkY + j >= chunkResY) { + chunkY++; + inChunkY = 0; + brushY = j % chunkResY; + } + inChunkY += brushY; + + if (chunkY < 0 || chunkY >= chunksPerY) { + GD.PrintErr($"Trying to write out of bounds: {x}:{y}"); + return; + } + + chunks[chunkX, chunkY].WritePixel(inChunkX, inChunkY); + } + } } public Image DrawLevel() { - for (int cx = 0; cx < chunksX; cx++) { - for (int cy = 0; cy < chunksY; cy++) { + for (int cx = 0; cx < chunksPerX; cx++) { + for (int cy = 0; cy < chunksPerY; cy++) { for (int x = 0; x < chunkResX; x++) { for (int y = 0; y < chunkResY; y++) { + // TODO: multithreading here! use Chunk.DrawLevel() and stitch images together image.SetPixel(cx*chunkResX + x, cy*chunkResY + y, chunks[cx,cy].Elements[x, y].Color); } } diff --git a/Scripts/Level.cs.uid b/Scripts/Level.cs.uid new file mode 100644 index 0000000..bc25504 --- /dev/null +++ b/Scripts/Level.cs.uid @@ -0,0 +1 @@ +uid://bkwbjagt8s01s diff --git a/Scripts/Main.cs b/Scripts/Main.cs index 9efc559..b8ca1c9 100644 --- a/Scripts/Main.cs +++ b/Scripts/Main.cs @@ -1,4 +1,3 @@ -using System.Reflection; using FOU.Scripts; using FOU.Scripts.Elements; using Godot; @@ -9,35 +8,31 @@ public partial class Main : Node2D { [Export] public int BrushSize = 5; [Export] public float TextureResolution = 0.5f; [Export] public int ChunksPerAxis = 2; - - [Export] - public float RainAmount { - get => rainAmount; - set { - rainAmount = value; - mLevel.SetRainAmount(rainAmount); - } - } + [Export] public float RainAmount = 0; public static Main Instance; + public Level Level; private TextureRect mLevelDrawer; - private Level mLevel; private bool enableRain; private float rainAmount; public override void _Ready() { - mLevel = new Level(this, (int)(GetViewportRect().Size.X * TextureResolution), + Level = new Level(this, (int)(GetViewportRect().Size.X * TextureResolution), (int)(GetViewportRect().Size.Y * TextureResolution)); - mLevel.SetRainAmount(rainAmount); + Level.SetRainAmount(rainAmount); mLevelDrawer = GetNode("CanvasLayer/LevelDrawer"); Instance = this; } + public override void _PhysicsProcess(double delta) { + base._PhysicsProcess(delta); + Level.Update(); + } + public override void _Process(double delta) { - mLevel.Update(); - mLevelDrawer.Texture = ImageTexture.CreateFromImage(mLevel.DrawLevel()); + mLevelDrawer.Texture = ImageTexture.CreateFromImage(Level.DrawLevel()); } public override void _UnhandledInput(InputEvent @event) { @@ -46,11 +41,15 @@ public partial class Main : Node2D { if (eventMouseButton.IsPressed()) { Vector2 mouse = GetViewport().GetMousePosition(); - float mappedX = mouse.X / (GetViewportRect().Size.X / mLevel.Resolution.X); - float mappedY = mouse.Y / (GetViewportRect().Size.Y / mLevel.Resolution.Y); + float mappedX = mouse.X / (GetViewportRect().Size.X / Level.Resolution.X); + float mappedY = mouse.Y / (GetViewportRect().Size.Y / Level.Resolution.Y); - mLevel.WritePixel((int)mappedX, (int)mappedY, BrushSize); + Level.WritePixel((int)mappedX, (int)mappedY, BrushSize); } - } else base._UnhandledInput(@event); + } else if (@event is InputEventKey keyEvent && keyEvent.Pressed) { + if (keyEvent.Keycode == Key.F9) + Level.StartBenchmark(); + } + else base._UnhandledInput(@event); } } diff --git a/Scripts/Main.cs.uid b/Scripts/Main.cs.uid new file mode 100644 index 0000000..75a805e --- /dev/null +++ b/Scripts/Main.cs.uid @@ -0,0 +1 @@ +uid://cmvvubxfvdca7 diff --git a/benchmark.txt b/benchmark.txt index e69de29..bc9fca6 100644 --- a/benchmark.txt +++ b/benchmark.txt @@ -0,0 +1,22 @@ +# Benchmark + +Start from editor not rider! + +## Settings + +- Texture Resolution: 0.5 +- CHunks per Axis: 2 +- Rain Amount: 0 + +## Measurements: + +### original (no optimization): + +- idle: 42 - 44 fps +- benchmark: 24 fps minimum + +### performance-improvements (71db6513f22094e7dd67bd32c7ab86b326f9ad9e) + +- idle before: 89 +- benchmark: 33 +- idle after: 75 diff --git a/project.godot b/project.godot index c221214..f95826d 100644 --- a/project.godot +++ b/project.godot @@ -12,7 +12,7 @@ config_version=5 config/name="FOU" run/main_scene="res://Scenes/main.tscn" -config/features=PackedStringArray("4.2", "C#", "Forward Plus") +config/features=PackedStringArray("4.4", "C#", "Forward Plus") config/icon="res://icon.svg" [dotnet]