├── .editorconfig ├── .gitignore ├── FosterSamples.sln ├── Froggymark ├── Assets │ ├── frog_knight.png │ └── monogram.ttf ├── Froggymark.csproj ├── Program.cs ├── README.md └── screenshot.png ├── ImGui ├── FosterImGui.csproj ├── Program.cs ├── README.md ├── Renderer.cs ├── button.png └── screenshot.png ├── LICENSE ├── README.md ├── Shapes ├── Program.cs ├── README.md ├── Shapes.csproj └── screenshot.png └── TinyLink ├── Assets ├── Fonts │ ├── dogica.ttf │ └── dogica_license.txt ├── Rooms │ ├── -1x0.txt │ ├── 0x0.txt │ ├── 10x0.txt │ ├── 11x0.txt │ ├── 12x0.txt │ ├── 13x0.txt │ ├── 1x0.txt │ ├── 2x0.txt │ ├── 3x0.txt │ ├── 3x1.txt │ ├── 4x1.txt │ ├── 5x1.txt │ ├── 6x1.txt │ ├── 7x1.txt │ ├── 8x0.txt │ ├── 8x1.txt │ └── 9x0.txt ├── Sprites │ ├── blob.ase │ ├── bramble.ase │ ├── bullet.ase │ ├── buttons.ase │ ├── circle.ase │ ├── door.ase │ ├── ghostfrog.ase │ ├── heart.ase │ ├── jumpthru.ase │ ├── mosquito.ase │ ├── player.ase │ ├── pop.ase │ └── spitter.ase └── Tilesets │ ├── back.ase │ ├── castle.ase │ ├── grass.ase │ ├── plants.ase │ └── water.ase ├── README.md ├── Source ├── Actors │ ├── Actor.cs │ ├── Blob.cs │ ├── Bramble.cs │ ├── Bullet.cs │ ├── Door.cs │ ├── GhostFrog.cs │ ├── Grid.cs │ ├── Jumpthru.cs │ ├── Mosquito.cs │ ├── Orb.cs │ ├── Player.cs │ ├── Pop.cs │ ├── Spitter.cs │ └── TitleText.cs ├── Assets │ ├── Assets.cs │ ├── Room.cs │ ├── Sprite.cs │ └── Tileset.cs ├── Controls.cs ├── Factory.cs ├── Game.cs ├── Hitbox.cs ├── Manager.cs └── screenshot.png ├── TinyLink.csproj └── screenshot.png /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | # use tabs by default for C#/C code 11 | [*.{cs,hpp,cpp,c,h}] 12 | indent_style = tab 13 | indent_size = 4 14 | 15 | # use 2-spaces for csproj 16 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] 17 | indent_style = space 18 | indent_size = 2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | .vs 4 | .vscode 5 | imgui.ini -------------------------------------------------------------------------------- /FosterSamples.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TinyLink", "TinyLink\TinyLink.csproj", "{AA3B2D39-7BF3-4CCD-8F19-672474E71D41}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shapes", "Shapes\Shapes.csproj", "{674B10E5-82F6-4A6A-8C00-83221996CD1E}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Froggymark", "Froggymark\Froggymark.csproj", "{C8D5CC24-2C60-4E8E-B246-8E7E13E31873}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FosterImGui", "ImGui\FosterImGui.csproj", "{ED95B28D-2822-4F3D-B1C5-591658BB2380}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {674B10E5-82F6-4A6A-8C00-83221996CD1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {674B10E5-82F6-4A6A-8C00-83221996CD1E}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {674B10E5-82F6-4A6A-8C00-83221996CD1E}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {674B10E5-82F6-4A6A-8C00-83221996CD1E}.Release|Any CPU.Build.0 = Release|Any CPU 27 | {C8D5CC24-2C60-4E8E-B246-8E7E13E31873}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {C8D5CC24-2C60-4E8E-B246-8E7E13E31873}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {C8D5CC24-2C60-4E8E-B246-8E7E13E31873}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {C8D5CC24-2C60-4E8E-B246-8E7E13E31873}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {ED95B28D-2822-4F3D-B1C5-591658BB2380}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {ED95B28D-2822-4F3D-B1C5-591658BB2380}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {ED95B28D-2822-4F3D-B1C5-591658BB2380}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {ED95B28D-2822-4F3D-B1C5-591658BB2380}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {AA3B2D39-7BF3-4CCD-8F19-672474E71D41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {AA3B2D39-7BF3-4CCD-8F19-672474E71D41}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {AA3B2D39-7BF3-4CCD-8F19-672474E71D41}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {AA3B2D39-7BF3-4CCD-8F19-672474E71D41}.Release|Any CPU.Build.0 = Release|Any CPU 39 | EndGlobalSection 40 | EndGlobal 41 | -------------------------------------------------------------------------------- /Froggymark/Assets/frog_knight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FosterFramework/Samples/5b97ca5329e61768f85a45d655da5df7f882519d/Froggymark/Assets/frog_knight.png -------------------------------------------------------------------------------- /Froggymark/Assets/monogram.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FosterFramework/Samples/5b97ca5329e61768f85a45d655da5df7f882519d/Froggymark/Assets/monogram.ttf -------------------------------------------------------------------------------- /Froggymark/Froggymark.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | enable 8 | true 9 | false 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | PreserveNewest 19 | 20 | 21 | PreserveNewest 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Froggymark/Program.cs: -------------------------------------------------------------------------------- 1 | using Foster.Framework; 2 | using System.Diagnostics; 3 | using System.Numerics; 4 | 5 | namespace Froggymark; 6 | 7 | class Program 8 | { 9 | public static void Main() 10 | { 11 | using var game = new Game(); 12 | game.Run(); 13 | } 14 | } 15 | 16 | class Game : App 17 | { 18 | private const int MaxFrogs = 1_000_000; 19 | private const int AddRemoveAmount = 5_000; 20 | private const int DrawBatchSize = 32768; 21 | 22 | private Rng rng = new(1337); 23 | private int frogCount = 0; 24 | private readonly Frog[] frogs = new Frog[MaxFrogs]; 25 | private readonly Mesh mesh; 26 | private readonly PosTexColVertex[] vertexArray = new PosTexColVertex[DrawBatchSize * 4]; 27 | private readonly Texture texture; 28 | private readonly Material material; 29 | private readonly Batcher batcher; 30 | private readonly SpriteFont font; 31 | private readonly FrameCounter frameCounter = new(); 32 | 33 | public Game() : base(new( 34 | ApplicationName: "Froggymark", 35 | WindowTitle: "Froggymark", 36 | Width: 1280, 37 | Height: 720)) 38 | { 39 | GraphicsDevice.VSync = true; 40 | Window.Resizable = false; 41 | UpdateMode = UpdateMode.UnlockedStep(); 42 | 43 | mesh = new(GraphicsDevice); 44 | batcher = new(GraphicsDevice); 45 | texture = new Texture(GraphicsDevice, new Image(Path.Join("Assets", "frog_knight.png"))); 46 | font = new SpriteFont(GraphicsDevice, Path.Join("Assets", "monogram.ttf"), 32); 47 | material = new Material(new TexturedShader(GraphicsDevice)); 48 | 49 | // We only need to initialize indices once, since we're only drawing quads 50 | var indexArray = new int[DrawBatchSize * 6]; 51 | var vertexCount = 0; 52 | 53 | for (int i = 0; i < indexArray.Length; i += 6) 54 | { 55 | indexArray[i + 0] = vertexCount + 0; 56 | indexArray[i + 1] = vertexCount + 1; 57 | indexArray[i + 2] = vertexCount + 2; 58 | indexArray[i + 3] = vertexCount + 0; 59 | indexArray[i + 4] = vertexCount + 2; 60 | indexArray[i + 5] = vertexCount + 3; 61 | vertexCount += 4; 62 | } 63 | 64 | mesh.SetIndices(indexArray); 65 | 66 | // Texture coordinates will not change, so we can initialize those 67 | for (int i = 0; i < DrawBatchSize * 4; i += 4) 68 | { 69 | vertexArray[i].Tex = new(0, 0); 70 | vertexArray[i + 1].Tex = new(1, 0); 71 | vertexArray[i + 2].Tex = new(1, 1); 72 | vertexArray[i + 3].Tex = new(0, 1); 73 | } 74 | } 75 | 76 | protected override void Startup() {} 77 | protected override void Shutdown() {} 78 | 79 | protected override void Update() 80 | { 81 | // Spawn frogs 82 | if (Input.Mouse.LeftDown) 83 | { 84 | for (int i = 0; i < AddRemoveAmount; i++) 85 | { 86 | if (frogCount < MaxFrogs) 87 | { 88 | frogs[frogCount].Position = Input.Mouse.Position; 89 | frogs[frogCount].Speed.X = rng.Float(-250, 250) / 60.0f; 90 | frogs[frogCount].Speed.Y = rng.Float(-250, 250) / 60.0f; 91 | frogs[frogCount].Color = new Color( 92 | rng.U8(50, 240), 93 | rng.U8(80, 240), 94 | rng.U8(100, 240), 95 | 255 96 | ); 97 | frogCount++; 98 | } 99 | } 100 | 101 | } 102 | // Remove frogs 103 | else if (Input.Mouse.RightDown) 104 | { 105 | frogCount = Math.Max(0, frogCount - AddRemoveAmount); 106 | } 107 | 108 | // Update frogs 109 | var halfSize = ((Vector2)texture.Size) / 2f; 110 | var screenSize = new Vector2(Window.WidthInPixels, Window.HeightInPixels); 111 | 112 | for (int i = 0; i < frogCount; i++) 113 | { 114 | frogs[i].Position += frogs[i].Speed; 115 | 116 | if (((frogs[i].Position.X + halfSize.X) > screenSize.X) || 117 | ((frogs[i].Position.X + halfSize.X) < 0)) 118 | { 119 | frogs[i].Speed.X *= -1; 120 | } 121 | 122 | if (((frogs[i].Position.Y + halfSize.Y) > screenSize.Y) || 123 | ((frogs[i].Position.Y + halfSize.Y - 40) < 0)) 124 | { 125 | frogs[i].Speed.Y *= -1; 126 | } 127 | } 128 | } 129 | 130 | protected override void Render() 131 | { 132 | frameCounter.Update(); 133 | 134 | Window.Clear(Color.White); 135 | 136 | batcher.Text(font, $"{frogCount} Frogs : {frameCounter.FPS} FPS", new(8, -2), Color.Black); 137 | batcher.Render(Window); 138 | batcher.Clear(); 139 | 140 | // Batching/batch size is important: too low = excessive draw calls, too high = slower gpu copies 141 | for (int i = 0; i < frogCount; i += DrawBatchSize) 142 | { 143 | var count = Math.Min(frogCount - i, DrawBatchSize); 144 | if (Input.Keyboard.Down(Keys.Space)) 145 | { 146 | RenderBatchCustom(i, count); 147 | } 148 | else 149 | { 150 | RenderBatchFoster(i, count); 151 | } 152 | } 153 | } 154 | 155 | /// 156 | /// Plain Foster. 157 | /// So simple, so fast. 158 | /// 159 | private void RenderBatchFoster(int from, int count) 160 | { 161 | for (int i = 0; i < count; i++) 162 | { 163 | var frog = frogs[i + from]; 164 | batcher.Image(texture, frog.Position, frog.Color); 165 | } 166 | batcher.Render(Window); 167 | batcher.Clear(); 168 | } 169 | 170 | /// 171 | /// A tailor made solution for shoving frogs into a gpu. 172 | /// Goes down a rabbit hole (ha) for a few extra frames: 173 | /// - Smaller vertex format 174 | /// - Simplified shader logic 175 | /// - Initialize indices only on startup 176 | /// - Initialize vertex texture coords on startup 177 | /// - One time shader uniform set per frame 178 | /// - A lot of inlining (same result could be achieved with AggressiveInlining) 179 | /// 180 | private void RenderBatchCustom(int from, int count) 181 | { 182 | for (int i = 0; i < count; i++) 183 | { 184 | var frog = frogs[i + from]; 185 | var v = i * 4; 186 | vertexArray[v].Col = frog.Color; 187 | vertexArray[v + 1].Col = frog.Color; 188 | vertexArray[v + 2].Col = frog.Color; 189 | vertexArray[v + 3].Col = frog.Color; 190 | vertexArray[v].Pos = frog.Position; 191 | vertexArray[v + 1].Pos = frog.Position + new Vector2(texture.Width, 0); 192 | vertexArray[v + 2].Pos = frog.Position + new Vector2(texture.Width, texture.Height); 193 | vertexArray[v + 3].Pos = frog.Position + new Vector2(0, texture.Height); 194 | } 195 | 196 | mesh.SetVertices(vertexArray.AsSpan(0, count * 4)); 197 | 198 | if (from == 0) 199 | { 200 | var matrix = Matrix4x4.CreateOrthographicOffCenter(0, Window.WidthInPixels, Window.HeightInPixels, 0, 0, float.MaxValue); 201 | material.Vertex.SetUniformBuffer(matrix); 202 | material.Fragment.Samplers[0] = new(texture, new()); 203 | } 204 | 205 | DrawCommand command = new(Window, mesh, material) 206 | { 207 | BlendMode = BlendMode.Premultiply, 208 | IndexOffset = 0, 209 | IndexCount = count * 6 210 | }; 211 | 212 | command.Submit(GraphicsDevice); 213 | } 214 | 215 | public struct Frog 216 | { 217 | public Vector2 Position; 218 | public Vector2 Speed; 219 | public Color Color; 220 | } 221 | } 222 | 223 | /// 224 | /// Simple utility to count frames in last second 225 | /// 226 | public class FrameCounter 227 | { 228 | public int FPS = 0; 229 | public int Frames = 0; 230 | public Stopwatch sw = Stopwatch.StartNew(); 231 | 232 | public void Update() 233 | { 234 | Frames++; 235 | var elapsed = sw.Elapsed.TotalSeconds; 236 | if (elapsed > 1) 237 | { 238 | sw.Restart(); 239 | FPS = Frames; 240 | Frames = 0; 241 | } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /Froggymark/README.md: -------------------------------------------------------------------------------- 1 | # Froggymark 2 | ![Screenshot](screenshot.png "Screenshot") 3 | 4 | This serves as a naive test to find just how fast sprites can be pushed to the gpu. 5 | 6 | **Use Release build without debugger for best results.** 7 | 8 | ## Controls 9 | - Left Mouse: Spawn frogs 10 | - Right Mouse: Remove frogs 11 | - Space: Enable custom renderer for a few extra frames 12 | 13 | ## Results 14 | - 340k (475k w/ custom renderer) frogs @ 60 fps on: 15 | - NVIDIA GeForce RTX 2060 with Max-Q Design/PCIe/SSE2 16 | - AMD Ryzen 9 4900HS 17 | 18 | ## Observations 19 | - Foster's default sprite batcher is performant and should serve most use cases well 20 | - Foster allows low level rendering via mesh buffers for any high performance needs 21 | 22 | ## Assets 23 | 24 | | asset | author | license | notes | 25 | | :------------------- | :---------: | :------: | :---- | 26 | | monogram.ttf | [datagoblin](https://datagoblin.itch.io/monogram/) | [CC0](https://creativecommons.org/publicdomain/zero/1.0/) | | 27 | | frog_knight.png | [NoelFB](https://github.com/NoelFB/tiny_link/tree/main) | [MIT](https://github.com/NoelFB/tiny_link/blob/main/LICENSE) | | 28 | 29 | ## Disclaimer 30 | _You should never base your evaluation of any engine/framework on a single benchmark alone, as an extremely specialized solution, such as this one, provides no indication of real world performance or usability._ -------------------------------------------------------------------------------- /Froggymark/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FosterFramework/Samples/5b97ca5329e61768f85a45d655da5df7f882519d/Froggymark/screenshot.png -------------------------------------------------------------------------------- /ImGui/FosterImGui.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | enable 8 | true 9 | false 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | PreserveNewest 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /ImGui/Program.cs: -------------------------------------------------------------------------------- 1 | using Foster.Framework; 2 | using System.Numerics; 3 | using ImGuiNET; 4 | 5 | namespace FosterImGui; 6 | 7 | class Program 8 | { 9 | public static void Main() 10 | { 11 | using var editor = new Editor(); 12 | editor.Run(); 13 | } 14 | } 15 | 16 | class Editor : App 17 | { 18 | private readonly Texture image; 19 | private readonly Renderer imRenderer; 20 | 21 | public Editor() : base(new AppConfig() 22 | { 23 | ApplicationName = "ImGuiExample", 24 | WindowTitle = "Dear ImGui x Foster", 25 | Width = 1280, 26 | Height = 720, 27 | Resizable = true 28 | }) 29 | { 30 | image = new Texture(GraphicsDevice, new Image("button.png")); 31 | imRenderer = new(this); 32 | } 33 | 34 | protected override void Startup() 35 | { 36 | } 37 | 38 | protected override void Shutdown() 39 | { 40 | imRenderer.Dispose(); 41 | } 42 | 43 | protected override void Update() 44 | { 45 | imRenderer.BeginLayout(); 46 | 47 | // toggle text input if ImGui wants it 48 | if (imRenderer.WantsTextInput) 49 | Window.StartTextInput(); 50 | else 51 | Window.StopTextInput(); 52 | 53 | ImGui.SetNextWindowSize(new Vector2(400, 300), ImGuiCond.Appearing); 54 | if (ImGui.Begin("Hello Foster x Dear ImGui")) 55 | { 56 | // show an Image button 57 | var imageId = imRenderer.GetTextureID(image); 58 | if (ImGui.ImageButton("Image", imageId, new Vector2(32, 32))) 59 | ImGui.OpenPopup("Image Button"); 60 | 61 | // image buttton popup 62 | if (ImGui.BeginPopup("Image Button")) 63 | { 64 | ImGui.Text("You pressed the Image Button!"); 65 | ImGui.EndPopup(); 66 | } 67 | 68 | // custom sprite batcher inside imgui window 69 | ImGui.Text("Some Foster Sprite Batching:"); 70 | var size = new Vector2(ImGui.GetContentRegionAvail().X, 200); 71 | if (imRenderer.BeginBatch(size, out var batch, out var bounds)) 72 | { 73 | batch.CheckeredPattern(bounds, 16, 16, Color.DarkGray, Color.Gray); 74 | batch.Circle(bounds.Center, 32, 16, Color.Red); 75 | } 76 | imRenderer.EndBatch(); 77 | 78 | ImGui.Text("That weas pretty cool!"); 79 | } 80 | ImGui.End(); 81 | 82 | ImGui.ShowDemoWindow(); 83 | 84 | imRenderer.EndLayout(); 85 | } 86 | 87 | protected override void Render() 88 | { 89 | Window.Clear(Color.Black); 90 | imRenderer.Render(); 91 | } 92 | } -------------------------------------------------------------------------------- /ImGui/README.md: -------------------------------------------------------------------------------- 1 | # Foster.ImGui 2 | ![Screenshot](screenshot.png "Screenshot") 3 | 4 | This is a sample implementation of ImGui.NET in Foster. 5 | 6 | This sample requires the [ImGui.NET Repository](https://github.com/ImGuiNET/ImGui.NET) which is included as a NuGet package automatically. -------------------------------------------------------------------------------- /ImGui/Renderer.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Numerics; 3 | using Foster.Framework; 4 | using ImGuiNET; 5 | 6 | namespace FosterImGui; 7 | 8 | public class Renderer : IDisposable 9 | { 10 | private readonly App app; 11 | private readonly IntPtr context; 12 | private readonly Mesh mesh; 13 | private readonly Material material; 14 | private readonly Texture fontTexture; 15 | private readonly List boundTextures = []; 16 | private readonly List batchersUsed = []; 17 | private readonly Stack batchersStack = []; 18 | private readonly Stack batcherPool = []; 19 | private readonly List<(ImGuiKey, Keys)> keys = 20 | [ 21 | (ImGuiKey.Tab, Keys.Tab), 22 | (ImGuiKey.LeftArrow, Keys.Left), 23 | (ImGuiKey.RightArrow, Keys.Right), 24 | (ImGuiKey.UpArrow, Keys.Up), 25 | (ImGuiKey.DownArrow, Keys.Down), 26 | (ImGuiKey.PageUp, Keys.PageUp), 27 | (ImGuiKey.PageDown, Keys.PageDown), 28 | (ImGuiKey.Home, Keys.Home), 29 | (ImGuiKey.End, Keys.End), 30 | (ImGuiKey.Insert, Keys.Insert), 31 | (ImGuiKey.Delete, Keys.Delete), 32 | (ImGuiKey.Backspace, Keys.Backspace), 33 | (ImGuiKey.Space, Keys.Space), 34 | (ImGuiKey.Enter, Keys.Enter), 35 | (ImGuiKey.Escape, Keys.Escape), 36 | (ImGuiKey.LeftCtrl, Keys.LeftControl), 37 | (ImGuiKey.LeftShift, Keys.LeftShift), 38 | (ImGuiKey.LeftAlt, Keys.LeftAlt), 39 | (ImGuiKey.LeftSuper, Keys.LeftOS), 40 | (ImGuiKey.RightCtrl, Keys.RightControl), 41 | (ImGuiKey.RightShift, Keys.RightShift), 42 | (ImGuiKey.RightAlt, Keys.RightAlt), 43 | (ImGuiKey.RightSuper, Keys.RightOS), 44 | (ImGuiKey.Menu, Keys.Menu), 45 | (ImGuiKey._0, Keys.D0), 46 | (ImGuiKey._1, Keys.D1), 47 | (ImGuiKey._2, Keys.D2), 48 | (ImGuiKey._3, Keys.D3), 49 | (ImGuiKey._4, Keys.D4), 50 | (ImGuiKey._5, Keys.D5), 51 | (ImGuiKey._6, Keys.D6), 52 | (ImGuiKey._7, Keys.D7), 53 | (ImGuiKey._8, Keys.D8), 54 | (ImGuiKey._9, Keys.D9), 55 | (ImGuiKey.A, Keys.A), 56 | (ImGuiKey.B, Keys.B), 57 | (ImGuiKey.C, Keys.C), 58 | (ImGuiKey.D, Keys.D), 59 | (ImGuiKey.E, Keys.E), 60 | (ImGuiKey.F, Keys.F), 61 | (ImGuiKey.G, Keys.G), 62 | (ImGuiKey.H, Keys.H), 63 | (ImGuiKey.I, Keys.I), 64 | (ImGuiKey.J, Keys.J), 65 | (ImGuiKey.K, Keys.K), 66 | (ImGuiKey.L, Keys.L), 67 | (ImGuiKey.M, Keys.M), 68 | (ImGuiKey.N, Keys.N), 69 | (ImGuiKey.O, Keys.O), 70 | (ImGuiKey.P, Keys.P), 71 | (ImGuiKey.Q, Keys.Q), 72 | (ImGuiKey.R, Keys.R), 73 | (ImGuiKey.S, Keys.S), 74 | (ImGuiKey.T, Keys.T), 75 | (ImGuiKey.U, Keys.U), 76 | (ImGuiKey.V, Keys.V), 77 | (ImGuiKey.W, Keys.W), 78 | (ImGuiKey.X, Keys.X), 79 | (ImGuiKey.Y, Keys.Y), 80 | (ImGuiKey.Z, Keys.Z), 81 | (ImGuiKey.F1, Keys.F1), 82 | (ImGuiKey.F2, Keys.F2), 83 | (ImGuiKey.F3, Keys.F3), 84 | (ImGuiKey.F4, Keys.F4), 85 | (ImGuiKey.F5, Keys.F5), 86 | (ImGuiKey.F6, Keys.F6), 87 | (ImGuiKey.F7, Keys.F7), 88 | (ImGuiKey.F8, Keys.F8), 89 | (ImGuiKey.F9, Keys.F9), 90 | (ImGuiKey.F10, Keys.F10), 91 | (ImGuiKey.F11, Keys.F11), 92 | (ImGuiKey.F12, Keys.F12), 93 | (ImGuiKey.Apostrophe, Keys.Apostrophe), 94 | (ImGuiKey.Comma, Keys.Comma), 95 | (ImGuiKey.Minus, Keys.Minus), 96 | (ImGuiKey.Period, Keys.Period), 97 | (ImGuiKey.Slash, Keys.Slash), 98 | (ImGuiKey.Semicolon, Keys.Semicolon), 99 | (ImGuiKey.Equal, Keys.Equals), 100 | (ImGuiKey.LeftBracket, Keys.LeftBracket), 101 | (ImGuiKey.Backslash, Keys.Backslash), 102 | (ImGuiKey.RightBracket, Keys.RightBracket), 103 | (ImGuiKey.GraveAccent, Keys.Tilde), 104 | (ImGuiKey.CapsLock, Keys.Capslock), 105 | (ImGuiKey.ScrollLock, Keys.ScrollLock), 106 | (ImGuiKey.NumLock, Keys.Numlock), 107 | (ImGuiKey.PrintScreen, Keys.PrintScreen), 108 | (ImGuiKey.Pause, Keys.Pause), 109 | (ImGuiKey.Keypad0, Keys.Keypad0), 110 | (ImGuiKey.Keypad1, Keys.Keypad1), 111 | (ImGuiKey.Keypad2, Keys.Keypad2), 112 | (ImGuiKey.Keypad3, Keys.Keypad3), 113 | (ImGuiKey.Keypad4, Keys.Keypad4), 114 | (ImGuiKey.Keypad5, Keys.Keypad5), 115 | (ImGuiKey.Keypad6, Keys.Keypad6), 116 | (ImGuiKey.Keypad7, Keys.Keypad7), 117 | (ImGuiKey.Keypad8, Keys.Keypad8), 118 | (ImGuiKey.Keypad9, Keys.Keypad9), 119 | (ImGuiKey.KeypadDecimal, Keys.KeypadPeroid), 120 | (ImGuiKey.KeypadDivide, Keys.KeypadDivide), 121 | (ImGuiKey.KeypadMultiply, Keys.KeypadMultiply), 122 | (ImGuiKey.KeypadSubtract, Keys.KeypadMinus), 123 | (ImGuiKey.KeypadAdd, Keys.KeypadPlus), 124 | (ImGuiKey.KeypadEnter, Keys.KeypadEnter), 125 | (ImGuiKey.KeypadEqual, Keys.KeypadEquals), 126 | ]; 127 | 128 | private PosTexColVertex[] vertices = []; 129 | private ushort[] indices = []; 130 | 131 | /// 132 | /// UI Scaling 133 | /// 134 | public float Scale = 2.0f; 135 | 136 | /// 137 | /// Mouse Position relative to ImGui elements 138 | /// 139 | public Vector2 MousePosition => app?.Input.Mouse.Position / Scale ?? Vector2.Zero; 140 | 141 | /// 142 | /// If the ImGui Context wants text input 143 | /// 144 | public bool WantsTextInput { get; private set; } 145 | 146 | public Renderer(App app, string? customFontPath = null) 147 | { 148 | this.app = app; 149 | 150 | // create imgui context 151 | context = ImGui.CreateContext(null); 152 | ImGui.SetCurrentContext(context); 153 | 154 | var io = ImGui.GetIO(); 155 | io.BackendFlags = ImGuiBackendFlags.None; 156 | io.ConfigFlags = ImGuiConfigFlags.DockingEnable; 157 | 158 | // load ImGui Font 159 | { 160 | if (customFontPath != null && File.Exists(customFontPath)) 161 | { 162 | io.Fonts.AddFontFromFileTTF(customFontPath, 64); 163 | io.FontGlobalScale = 16.0f / 64.0f; 164 | } 165 | else 166 | { 167 | io.Fonts.AddFontDefault(); 168 | } 169 | } 170 | 171 | // create font texture 172 | unsafe 173 | { 174 | io.Fonts.GetTexDataAsRGBA32(out byte* pixelData, out int width, out int height, out int bytesPerPixel); 175 | fontTexture = new Texture(app.GraphicsDevice, width, height, new ReadOnlySpan(pixelData, width * height * 4)); 176 | } 177 | 178 | // create drawing resources 179 | mesh = new Mesh(app.GraphicsDevice); 180 | material = new(new TexturedShader(app.GraphicsDevice)); 181 | ImGui.SetCurrentContext(nint.Zero); 182 | } 183 | 184 | ~Renderer() => Dispose(); 185 | 186 | /// 187 | /// Begins a new ImGui Frame. 188 | /// ImGui methods are available between BeginLayout and EndLayout. 189 | /// 190 | public void BeginLayout() 191 | { 192 | Debug.Assert(ImGui.GetCurrentContext() == nint.Zero); 193 | ImGui.SetCurrentContext(context); 194 | 195 | // clear textures for the next frame 196 | boundTextures.Clear(); 197 | 198 | // clear batches 199 | batchersStack.Clear(); 200 | batchersUsed.ForEach(batcherPool.Push); 201 | batchersUsed.Clear(); 202 | 203 | // assign font texture again 204 | var io = ImGui.GetIO(); 205 | io.Fonts.SetTexID(GetTextureID(fontTexture)); 206 | 207 | // setup io 208 | io.DeltaTime = app.Time.Delta; 209 | io.DisplaySize = new Vector2(app.Window.WidthInPixels / Scale, app.Window.HeightInPixels / Scale); 210 | io.DisplayFramebufferScale = Vector2.One * Scale; 211 | 212 | io.AddMousePosEvent(MousePosition.X, MousePosition.Y); 213 | io.AddMouseButtonEvent(0, app.Input.Mouse.LeftDown || app.Input.Mouse.LeftPressed); 214 | io.AddMouseButtonEvent(1, app.Input.Mouse.RightDown || app.Input.Mouse.RightPressed); 215 | io.AddMouseButtonEvent(2, app.Input.Mouse.MiddleDown || app.Input.Mouse.MiddlePressed); 216 | io.AddMouseWheelEvent(app.Input.Mouse.Wheel.X, app.Input.Mouse.Wheel.Y); 217 | 218 | foreach (var k in keys) 219 | { 220 | if (app.Input.Keyboard.Pressed(k.Item2)) 221 | io.AddKeyEvent(k.Item1, true); 222 | if (app.Input.Keyboard.Released(k.Item2)) 223 | io.AddKeyEvent(k.Item1, false); 224 | } 225 | 226 | io.AddKeyEvent(ImGuiKey.ModShift, app.Input.Keyboard.Shift); 227 | io.AddKeyEvent(ImGuiKey.ModAlt, app.Input.Keyboard.Alt); 228 | io.AddKeyEvent(ImGuiKey.ModCtrl, app.Input.Keyboard.Ctrl); 229 | io.AddKeyEvent(ImGuiKey.ModSuper, app.Input.Keyboard.Down(Keys.LeftOS) || app.Input.Keyboard.Down(Keys.RightOS)); 230 | 231 | if (app.Input.Keyboard.Text.Length > 0) 232 | { 233 | for (int i = 0; i < app.Input.Keyboard.Text.Length; i++) 234 | io.AddInputCharacter(app.Input.Keyboard.Text[i]); 235 | } 236 | 237 | WantsTextInput = io.WantTextInput; 238 | 239 | ImGui.NewFrame(); 240 | } 241 | 242 | /// 243 | /// Ends an ImGui Frame. 244 | /// Call this at the end of your Update method. 245 | /// 246 | public void EndLayout() 247 | { 248 | Debug.Assert(ImGui.GetCurrentContext() == context); 249 | ImGui.Render(); 250 | ImGui.SetCurrentContext(nint.Zero); 251 | } 252 | 253 | /// 254 | /// Begin a new Batch in an ImGui Window. Returns true if any batch contents 255 | /// will be visible. Call regardless of return value. 256 | /// 257 | public bool BeginBatch(out Batcher batch, out Rect bounds) 258 | { 259 | return BeginBatch(ImGui.GetContentRegionAvail(), out batch, out bounds); 260 | } 261 | 262 | /// 263 | /// Begin a new Batch in an ImGui Window. Returns true if any batch contents 264 | /// will be visible. Call regardless of return value. 265 | /// 266 | public bool BeginBatch(Vector2 size, out Batcher batch, out Rect bounds) 267 | { 268 | var min = ImGui.GetCursorScreenPos(); 269 | var max = min + size; 270 | var screenspace = Rect.Between(min, max); 271 | var clip = Rect.Between(ImGui.GetWindowDrawList().GetClipRectMin(), ImGui.GetWindowDrawList().GetClipRectMax()); 272 | var scissor = screenspace.GetIntersection(clip).Scale(Scale).Int(); 273 | 274 | // create a dummy element of the given size 275 | ImGui.Dummy(size); 276 | 277 | // get recycled batcher, add to list 278 | batch = batcherPool.Count > 0 ? batcherPool.Pop() : new Batcher(app.GraphicsDevice); 279 | batch.Clear(); 280 | batchersUsed.Add(batch); 281 | batchersStack.Push(batch); 282 | 283 | // notify imgui 284 | ImGui.GetWindowDrawList().AddCallback(new IntPtr(batchersUsed.Count), new IntPtr(0)); 285 | 286 | // push relative coords 287 | batch.PushScissor(scissor); 288 | batch.PushMatrix(Matrix3x2.CreateScale(Scale)); 289 | batch.PushMatrix(screenspace.TopLeft); 290 | 291 | bounds = new Rect(0, 0, screenspace.Width, screenspace.Height); 292 | 293 | return scissor.Width > 0 && scissor.Height > 0; 294 | } 295 | 296 | /// 297 | /// End a Batch in an ImGui Window 298 | /// 299 | public void EndBatch() 300 | { 301 | var batch = batchersStack.Pop(); 302 | batch.PopMatrix(); 303 | batch.PopMatrix(); 304 | batch.PopScissor(); 305 | } 306 | 307 | /// 308 | /// Renders the ImGui buffers. Call this in your Render method. 309 | /// 310 | public unsafe void Render() 311 | { 312 | Debug.Assert(ImGui.GetCurrentContext() == nint.Zero); 313 | ImGui.SetCurrentContext(context); 314 | 315 | var data = ImGui.GetDrawData(); 316 | if (data.NativePtr == null || data.TotalVtxCount <= 0) 317 | { 318 | ImGui.SetCurrentContext(nint.Zero); 319 | return; 320 | } 321 | 322 | // build vertex/index buffer lists 323 | { 324 | // calculate total size 325 | var vertexCount = 0; 326 | var indexCount = 0; 327 | for (int i = 0; i < data.CmdListsCount; i ++) 328 | { 329 | vertexCount += data.CmdLists[i].VtxBuffer.Size; 330 | indexCount += data.CmdLists[i].IdxBuffer.Size; 331 | } 332 | 333 | // make sure we have enough space 334 | if (vertexCount > vertices.Length) 335 | Array.Resize(ref vertices, vertexCount); 336 | if (indexCount > indices.Length) 337 | Array.Resize(ref indices, indexCount); 338 | 339 | // copy data to arrays 340 | vertexCount = indexCount = 0; 341 | for (int i = 0; i < data.CmdListsCount; i ++) 342 | { 343 | var list = data.CmdLists[i]; 344 | var vertexSrc = new Span((void*)list.VtxBuffer.Data, list.VtxBuffer.Size); 345 | var indexSrc = new Span((void*)list.IdxBuffer.Data, list.IdxBuffer.Size); 346 | 347 | vertexSrc.CopyTo(vertices.AsSpan()[vertexCount..]); 348 | indexSrc.CopyTo(indices.AsSpan()[indexCount..]); 349 | 350 | vertexCount += vertexSrc.Length; 351 | indexCount += indexSrc.Length; 352 | } 353 | 354 | // upload buffers to mesh 355 | mesh.SetVertices(vertices.AsSpan(0, vertexCount)); 356 | mesh.SetIndices(indices.AsSpan(0, indexCount)); 357 | } 358 | 359 | var size = new Point2(app.Window.WidthInPixels, app.Window.HeightInPixels); 360 | 361 | // create pass 362 | var pass = new DrawCommand(app.Window, mesh, material); 363 | pass.BlendMode = new BlendMode(BlendOp.Add, BlendFactor.SrcAlpha, BlendFactor.OneMinusSrcAlpha); 364 | 365 | // setup ortho matrix 366 | Matrix4x4 mat = 367 | Matrix4x4.CreateScale(data.FramebufferScale.X, data.FramebufferScale.Y, 1.0f) * 368 | Matrix4x4.CreateOrthographicOffCenter(0, size.X, size.Y, 0, 0.1f, 1000.0f); 369 | material.Vertex.SetUniformBuffer(mat); 370 | 371 | // draw imgui buffers to the screen 372 | var globalVtxOffset = 0; 373 | var globalIdxOffset = 0; 374 | for (int i = 0; i < data.CmdListsCount; i++) 375 | { 376 | var imList = data.CmdLists[i]; 377 | var imCommands = (ImDrawCmd*)imList.CmdBuffer.Data; 378 | 379 | // draw each command 380 | for (ImDrawCmd* cmd = imCommands; cmd < imCommands + imList.CmdBuffer.Size; cmd++) 381 | { 382 | var scissor = new Rect( 383 | cmd->ClipRect.X, 384 | cmd->ClipRect.Y, 385 | cmd->ClipRect.Z - cmd->ClipRect.X, 386 | cmd->ClipRect.W - cmd->ClipRect.Y).Scale(data.FramebufferScale).Int(); 387 | 388 | if (scissor.Width <= 0 || scissor.Height <= 0) 389 | continue; 390 | 391 | if (cmd->UserCallback != IntPtr.Zero) 392 | { 393 | var batchIndex = cmd->UserCallback.ToInt32() - 1; 394 | if (batchIndex >= 0 && batchIndex < batchersUsed.Count) 395 | batchersUsed[batchIndex].Render(app.Window, viewport: null, scissor: scissor); 396 | } 397 | else 398 | { 399 | var textureIndex = cmd->TextureId.ToInt32(); 400 | if (textureIndex < boundTextures.Count) 401 | material.Fragment.Samplers[0] = new(boundTextures[textureIndex], new()); 402 | 403 | pass.VertexOffset = (int)(cmd->VtxOffset + globalVtxOffset); 404 | pass.IndexOffset = (int)(cmd->IdxOffset + globalIdxOffset); 405 | pass.IndexCount = (int)cmd->ElemCount; 406 | pass.Scissor = scissor; 407 | 408 | app.GraphicsDevice.Draw(pass); 409 | } 410 | } 411 | 412 | globalVtxOffset += imList.VtxBuffer.Size; 413 | globalIdxOffset += imList.IdxBuffer.Size; 414 | } 415 | 416 | ImGui.SetCurrentContext(nint.Zero); 417 | } 418 | 419 | /// 420 | /// Gets a Texture ID to draw in ImGui 421 | /// 422 | public IntPtr GetTextureID(Texture? texture) 423 | { 424 | var id = new IntPtr(boundTextures.Count); 425 | if (texture != null) 426 | boundTextures.Add(texture); 427 | return id; 428 | } 429 | 430 | public void Dispose() 431 | { 432 | GC.SuppressFinalize(this); 433 | ImGui.DestroyContext(context); 434 | } 435 | } 436 | -------------------------------------------------------------------------------- /ImGui/button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FosterFramework/Samples/5b97ca5329e61768f85a45d655da5df7f882519d/ImGui/button.png -------------------------------------------------------------------------------- /ImGui/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FosterFramework/Samples/5b97ca5329e61768f85a45d655da5df7f882519d/ImGui/screenshot.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Noel Berry 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Foster Samples 2 | Samples Projects and Demos for the C# [Foster Framework](https://github.com/FosterFramework/Foster). 3 | 4 | | Sample | Description | Preview | 5 | | --- | --- | --- | 6 | | [Shapes](https://github.com/FosterFramework/Samples/tree/main/Shapes) | A small program that draws shapes to the screen without loading any assets | | 7 | | [Froggymark](https://github.com/FosterFramework/Samples/tree/main/Froggymark) | Performance test that draws as many quads to the screen as it can | | 8 | | [ImGui](https://github.com/FosterFramework/Samples/tree/main/ImGui) | An example using ImGui.NET wrapped in Foster draw calls | | 9 | | [TinyLink](https://github.com/FosterFramework/Samples/tree/main/TinyLink) | A 2D platformer with a small level editor | | 10 | 11 | -------------------------------------------------------------------------------- /Shapes/Program.cs: -------------------------------------------------------------------------------- 1 |  2 | using Foster.Framework; 3 | using System.Numerics; 4 | 5 | using var game = new Game(); 6 | game.Run(); 7 | 8 | class Game : App 9 | { 10 | private const float Acceleration = 1200; 11 | private const float Friction = 500; 12 | private const float MaxSpeed = 800; 13 | 14 | private readonly Batcher batch; 15 | private readonly Texture texture; 16 | private Vector2 pos = new(128, 128); 17 | private Vector2 speed = new(); 18 | 19 | public Game() : base(new AppConfig() 20 | { 21 | ApplicationName = "Shapes", 22 | WindowTitle = "Hello Shapes", 23 | Width = 1280, 24 | Height = 720 25 | }) 26 | { 27 | batch = new(GraphicsDevice); 28 | texture = new(GraphicsDevice, new Image(128, 128, Color.Blue)); 29 | } 30 | 31 | protected override void Startup() {} 32 | protected override void Shutdown() {} 33 | 34 | protected override void Update() 35 | { 36 | Window.Title = $"Something Else {Window.Width}x{Window.Height} : {Window.WidthInPixels}x{Window.HeightInPixels}"; 37 | 38 | if (Input.Keyboard.Down(Keys.Left)) 39 | speed.X -= Acceleration * Time.Delta; 40 | if (Input.Keyboard.Down(Keys.Right)) 41 | speed.X += Acceleration * Time.Delta; 42 | if (Input.Keyboard.Down(Keys.Up)) 43 | speed.Y -= Acceleration * Time.Delta; 44 | if (Input.Keyboard.Down(Keys.Down)) 45 | speed.Y += Acceleration * Time.Delta; 46 | 47 | if (!Input.Keyboard.Down(Keys.Left, Keys.Right)) 48 | speed.X = Calc.Approach(speed.X, 0, Time.Delta * Friction); 49 | if (!Input.Keyboard.Down(Keys.Up, Keys.Down)) 50 | speed.Y = Calc.Approach(speed.Y, 0, Time.Delta * Friction); 51 | 52 | if (Input.Keyboard.Pressed(Keys.F4)) 53 | Window.Fullscreen = !Window.Fullscreen; 54 | 55 | if (speed.Length() > MaxSpeed) 56 | speed = speed.Normalized() * MaxSpeed; 57 | 58 | pos += speed * Time.Delta; 59 | } 60 | 61 | protected override void Render() 62 | { 63 | Window.Clear(new Color( 64 | Calc.Clamp(Input.Mouse.X / Window.WidthInPixels, 0, 1), 65 | Calc.Clamp(Input.Mouse.Y / Window.HeightInPixels, 0, 1), 66 | 0.25f, 67 | 1.0f 68 | )); 69 | 70 | batch.PushMatrix( 71 | new Vector2(Window.WidthInPixels, Window.HeightInPixels) / 2, 72 | new Vector2(texture.Width, texture.Height) / 2, 73 | Vector2.One, 74 | (float)Time.Elapsed.TotalSeconds * 4.0f); 75 | 76 | batch.Image(texture, Vector2.Zero, Color.White); 77 | batch.PopMatrix(); 78 | 79 | batch.Circle(new Circle(pos, 64), 16, Color.Red); 80 | batch.Circle(new Circle(Input.Mouse.Position, 8), 16, Color.White); 81 | 82 | batch.Render(Window); 83 | batch.Clear(); 84 | } 85 | } -------------------------------------------------------------------------------- /Shapes/README.md: -------------------------------------------------------------------------------- 1 | # Shapes 2 | ![Screenshot](screenshot.png "Screenshot") 3 | 4 | A simple application that draws shapes to the screen. -------------------------------------------------------------------------------- /Shapes/Shapes.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | enable 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Shapes/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FosterFramework/Samples/5b97ca5329e61768f85a45d655da5df7f882519d/Shapes/screenshot.png -------------------------------------------------------------------------------- /TinyLink/Assets/Fonts/dogica.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FosterFramework/Samples/5b97ca5329e61768f85a45d655da5df7f882519d/TinyLink/Assets/Fonts/dogica.ttf -------------------------------------------------------------------------------- /TinyLink/Assets/Fonts/dogica_license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020, Roberto Mocci (), 2 | with Reserved Font Name Dogica. 3 | 4 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 5 | This license is copied below, and is also available with a FAQ at: 6 | http://scripts.sil.org/OFL 7 | 8 | 9 | ----------------------------------------------------------- 10 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 11 | ----------------------------------------------------------- 12 | 13 | PREAMBLE 14 | The goals of the Open Font License (OFL) are to stimulate worldwide 15 | development of collaborative font projects, to support the font creation 16 | efforts of academic and linguistic communities, and to provide a free and 17 | open framework in which fonts may be shared and improved in partnership 18 | with others. 19 | 20 | The OFL allows the licensed fonts to be used, studied, modified and 21 | redistributed freely as long as they are not sold by themselves. The 22 | fonts, including any derivative works, can be bundled, embedded, 23 | redistributed and/or sold with any software provided that any reserved 24 | names are not used by derivative works. The fonts and derivatives, 25 | however, cannot be released under any other type of license. The 26 | requirement for fonts to remain under this license does not apply 27 | to any document created using the fonts or their derivatives. 28 | 29 | DEFINITIONS 30 | "Font Software" refers to the set of files released by the Copyright 31 | Holder(s) under this license and clearly marked as such. This may 32 | include source files, build scripts and documentation. 33 | 34 | "Reserved Font Name" refers to any names specified as such after the 35 | copyright statement(s). 36 | 37 | "Original Version" refers to the collection of Font Software components as 38 | distributed by the Copyright Holder(s). 39 | 40 | "Modified Version" refers to any derivative made by adding to, deleting, 41 | or substituting -- in part or in whole -- any of the components of the 42 | Original Version, by changing formats or by porting the Font Software to a 43 | new environment. 44 | 45 | "Author" refers to any designer, engineer, programmer, technical 46 | writer or other person who contributed to the Font Software. 47 | 48 | PERMISSION & CONDITIONS 49 | Permission is hereby granted, free of charge, to any person obtaining 50 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 51 | redistribute, and sell modified and unmodified copies of the Font 52 | Software, subject to the following conditions: 53 | 54 | 1) Neither the Font Software nor any of its individual components, 55 | in Original or Modified Versions, may be sold by itself. 56 | 57 | 2) Original or Modified Versions of the Font Software may be bundled, 58 | redistributed and/or sold with any software, provided that each copy 59 | contains the above copyright notice and this license. These can be 60 | included either as stand-alone text files, human-readable headers or 61 | in the appropriate machine-readable metadata fields within text or 62 | binary files as long as those fields can be easily viewed by the user. 63 | 64 | 3) No Modified Version of the Font Software may use the Reserved Font 65 | Name(s) unless explicit written permission is granted by the corresponding 66 | Copyright Holder. This restriction only applies to the primary font name as 67 | presented to the users. 68 | 69 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 70 | Software shall not be used to promote, endorse or advertise any 71 | Modified Version, except to acknowledge the contribution(s) of the 72 | Copyright Holder(s) and the Author(s) or with their explicit written 73 | permission. 74 | 75 | 5) The Font Software, modified or unmodified, in part or in whole, 76 | must be distributed entirely under this license, and must not be 77 | distributed under any other license. The requirement for fonts to 78 | remain under this license does not apply to any document created 79 | using the Font Software. 80 | 81 | TERMINATION 82 | This license becomes null and void if any of the above conditions are 83 | not met. 84 | 85 | DISCLAIMER 86 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 87 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 88 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 89 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 90 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 91 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 92 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 93 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 94 | OTHER DEALINGS IN THE FONT SOFTWARE. -------------------------------------------------------------------------------- /TinyLink/Assets/Rooms/-1x0.txt: -------------------------------------------------------------------------------- 1 | 000000000000000000000000000000 2 | 000000000000000000000000000000 3 | 000000000000000000000000000000 4 | 000000000000000000000000000000 5 | 000000000000000000000000000000 6 | GGG000000000000000000000000000 7 | GGG000000000000000000000000000 8 | GGG00000000000000000000000GGGG 9 | GGGG00000000000000000000ggGGGG 10 | GGGG00000000000000000000GGGGGG 11 | GGGG000000000000000000g0GGGGGG 12 | GGGG000P00000000000000GGGGGGGG 13 | GGGG--GGG-------------GGGGGGGG 14 | GGGGwwGGGwwwwwwwwwGGGGGGGGGGGG 15 | GGGGGGGGGGGGGGGwwwGGGGGGGGGGGG 16 | GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG 17 | GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG 18 | -------------------------------------------------------------------------------- /TinyLink/Assets/Rooms/0x0.txt: -------------------------------------------------------------------------------- 1 | :SWORD II: ADVENTURE OF FROG 2 | :arrow keys + X/C 3 | :stick + A/X 4 | 000000000000000000000000000000 5 | 000000000000000000000000000000 6 | 000000000000000000000000000000 7 | 000000000000000000000000000000 8 | 00000000000000T000000000000000 9 | 000000000000000000000000000000 10 | 0000000000000000000000000000## 11 | GG0000000000000000000000000### 12 | GG-0000###00000000000000000### 13 | GG00000###000000000000000BB### 14 | GG-0000####000000000000--11111 15 | GGgggg0####P0gg000gg00###11111 16 | GGGGGGGGG11111GGGGGGGGGGGGGGGG 17 | GGGGGGGGG11111GGGGGGGGGGGGGGGG 18 | GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG 19 | GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG 20 | GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG 21 | -------------------------------------------------------------------------------- /TinyLink/Assets/Rooms/10x0.txt: -------------------------------------------------------------------------------- 1 | 000000000000000011111111111111 2 | 000000000000000011111111111111 3 | 000000000000000011111111111111 4 | 000000000000000011111111111111 5 | 000000000000000011111111111111 6 | 000000000000000011111111111111 7 | 00000000000000001############1 8 | 00000000000000000###00##00#### 9 | 0000000000000000S###00##00#### 10 | 000000000000001111##11##11##11 11 | 0000000000g0111111##11##11##11 12 | 000000000011111111##11##11#011 13 | 0P000gg01111111111##11##110011 14 | 111111111111111111##11##110#11 15 | 111111111111111111##11##110#11 16 | GGGGGGGG1111111111#011##11##11 17 | GGGGGGGG11111111110011##11##11 18 | -------------------------------------------------------------------------------- /TinyLink/Assets/Rooms/11x0.txt: -------------------------------------------------------------------------------- 1 | 111111111111111111111111111111 2 | 111111111111111111111111111111 3 | 111111111111111111111111111111 4 | 111111111111111111111111111111 5 | 111111111111111111111111111111 6 | 111111111111111111111111111111 7 | 1############################1 8 | 00##00##00##00##00##00##00##00 9 | 0P##00##00##00##b0##b0##b0##00 10 | 111------------------------111 11 | 111000000000000000000000000111 12 | 110000000000000000000000000011 13 | 110000000000000000000000000011 14 | 11#000000000000000000000000#11 15 | 11##0000000000000000000000##11 16 | 11##0000000000000000000000##11 17 | 11##0000000000000000000000##11 18 | -------------------------------------------------------------------------------- /TinyLink/Assets/Rooms/12x0.txt: -------------------------------------------------------------------------------- 1 | 111111111111111111111111111111 2 | 111111111111111111111111111111 3 | 1111#######11####11#######1111 4 | 111########################111 5 | 111########################111 6 | 111#####00####00####00#####111 7 | 111####0000##0000##0000####111 8 | #######0000##0000##0000####111 9 | #C#####0000##0000##0000####111 10 | 111####0000##0000##0000####111 11 | 111####0000##0000##0000####111 12 | 111####0000##0000##0000####### 13 | 111####00P0##0F00##0000#####D# 14 | 1111111----11----11----1111111 15 | 1111111wwww11wwww11wwww1111111 16 | 1111111wwww11wwww11wwww1111111 17 | 1111111wwww11wwww11wwww1111111 18 | -------------------------------------------------------------------------------- /TinyLink/Assets/Rooms/13x0.txt: -------------------------------------------------------------------------------- 1 | :YOU SAVED POND 2 | :AND YOU ARE 3 | :A REAL HERO 4 | 111000000000000000000000000000 5 | 111000000000000000000000000000 6 | 111000000000000000000000000000 7 | 111000000000000000000000000000 8 | 111000000000000000000000000000 9 | 1110000000000000T0000000000000 10 | 111000000000000000000000000000 11 | 111000000000000000000000000000 12 | 111000000000000000000000000000 13 | 111000000000000000000000000000 14 | 111000000000000000000000000000 15 | ###000000000000000000000000000 16 | ###0P0000000000000000000000000 17 | 1111111---11GG---------------- 18 | 1111111www11GGwwwwwwwwwwwwwwww 19 | 11111wwwwwwGGGwwwwwwwwwwwwwwww 20 | 11111wwwwwwwGGGwwwwwwwwwwwwwww 21 | -------------------------------------------------------------------------------- /TinyLink/Assets/Rooms/1x0.txt: -------------------------------------------------------------------------------- 1 | 000000000000000000000000000000 2 | 000000000000000000000000000000 3 | 000000000000000000000000000000 4 | 000000000000000000000000000000 5 | 000000000000000000000000000000 6 | 000000000000000000000000000000 7 | ##0000000110000000000000000000 8 | ###00000#110000000000000000000 9 | ####00#####00000000####0000000 10 | ##P######000000000#######00000 11 | 111111#00000000000##00###00000 12 | 111111#ggBB00ggg0g##gg#S#ggg00 13 | GGGG1111GGGGGGGGG111111111GGGG 14 | GGGG1111GGGGGGGGG111111111GGGG 15 | GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG 16 | GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG 17 | GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG 18 | -------------------------------------------------------------------------------- /TinyLink/Assets/Rooms/2x0.txt: -------------------------------------------------------------------------------- 1 | 000000000000000000000000000000 2 | 000000000000000000000000000000 3 | 000000000000000000000000000000 4 | 000000000000000000000000000000 5 | 000000000000000000000000000000 6 | 000000000000000000000000000000 7 | 000000000000000gS00000000S0000 8 | 000000000000000110000000110000 9 | 00000000000000#11#00000011#000 10 | 00000000000000######0000###000 11 | 0000000000000##0####0000###000 12 | 000P00ggg0000#gBB###0ggBB##gg0 13 | GGGGGGGGGGGGG1111GG111GGGGGGGG 14 | GGGGGGGGGGGGG111GGGG11GGGGGGGG 15 | GGGGGGGGGGGGG1GGGGGGGGGGGGGGGG 16 | GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG 17 | GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG 18 | -------------------------------------------------------------------------------- /TinyLink/Assets/Rooms/3x0.txt: -------------------------------------------------------------------------------- 1 | 000000000000000000000000000000 2 | 000000000000000000000000000000 3 | 000000000000000000000000000000 4 | 000000000000000000000000000000 5 | 000000000000000000000000000000 6 | 000000000000000000000000000000 7 | 00000000000000000000000000###0 8 | 0000000000000000000000000##### 9 | 0000000000000000000000000###11 10 | 0000000000000000000M00000###11 11 | ##000000001111000000##00000#11 12 | ###P00ggg01111ggg00###0000gg11 13 | GGGGGGGGGGGGG111GGG1110001111G 14 | GGGGGGGGGGGGGGGGGGGG11##01111G 15 | GGGGGGGGGGGGGGGGGGGG11###11GGG 16 | GGGGGGGGGGGGGGGGGGG111###11GGG 17 | GGGGGGGGGGGGGGGGGGG111###11GGG 18 | -------------------------------------------------------------------------------- /TinyLink/Assets/Rooms/3x1.txt: -------------------------------------------------------------------------------- 1 | GGGGGGGGGGGGGG1GGGGG11###11GGG 2 | GGGGGGGGGGGGGGGGGGGG110P#11GGG 3 | GGGGGGGGGGGGGG00GGGG11--011GGG 4 | GG000GGGGGGGGG00GGGG11##011GGG 5 | GG000GGGGGGGGG11GGGG11#--11GGG 6 | GG000GGGGGGGGGGGGGGG11###111GG 7 | GG111GGGGGGGGGG1111G11###111GG 8 | GGGGGGGG11GGGG11111111###11GGG 9 | GGGGGGGG11GGGG11111110##011GGG 10 | GGGGGGGGGGGGGG1100000###011111 11 | GGGGGGGGGGGGG111#0BB00###11111 12 | GGGGGGGGGGGGG111##111111111### 13 | GGGGGGGGGGGGGG11##11111110#### 14 | GGGGGGGGGG000G11##00###0000### 15 | GGGGGGGGGG0M0G11##00###0BB0011 16 | GGGGGGGGGG000G1111111111111111 17 | GGGGGGGGGG111G1111111111111111 18 | -------------------------------------------------------------------------------- /TinyLink/Assets/Rooms/4x1.txt: -------------------------------------------------------------------------------- 1 | GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG 2 | GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG 3 | GGGGGG11GGGGGGGGGGGGGGGGGGGGGG 4 | GGGGGG11GGGGGGGGGGGGGGGGGGGGGG 5 | GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG 6 | GGGGGGGGGGGGGGGGGGG11111111111 7 | GGGGGGGGGGGGG11GGGG111######11 8 | GGG111111111111111111#####0000 9 | GGG111111111######11#####0BB00 10 | 11111####00####0000####1111111 11 | 11110###000####00BB####1111111 12 | ###000000000##0001111111111GGG 13 | ###000000000##0S011111111GGGGG 14 | #P00000###0M--11111GGGGGGGGGGG 15 | 1100000###BB##11111GGGG11GGGGG 16 | 1111111111111111GGGGGGG11GGGGG 17 | 1111111111111111GGGGGGGGGGGGGG 18 | -------------------------------------------------------------------------------- /TinyLink/Assets/Rooms/5x1.txt: -------------------------------------------------------------------------------- 1 | GGGGG11111111111111111111GGGGG 2 | GGGGG1111111111111111111111GGG 3 | GGGGG11###00#############11GGG 4 | GG11111##0000############11GGG 5 | GG1111##0000##000########111GG 6 | 11111####0000#00000##0###11111 7 | 1111#####000000000000M####1111 8 | 0000####000000#0000000###00000 9 | 0P000##000000###0000M00###D000 10 | 1111111000011####1100001111111 11 | 1111111##0#11####11#0001111111 12 | GGGGG11################11GGGGG 13 | GGGGG11##0000000##00###11GGGGG 14 | GGGGG1100000000000000##11GGGGG 15 | GGGGG11000000000000000011GGGGG 16 | GGGGG11000000000000000011GGGGG 17 | GGGGG11000000000000000011GGGGG 18 | -------------------------------------------------------------------------------- /TinyLink/Assets/Rooms/6x1.txt: -------------------------------------------------------------------------------- 1 | GGGGG1111111111111111111111111 2 | GGG111111111111111111111111111 3 | G111111###00##############0000 4 | 111######00000############0000 5 | 11######000000000########00000 6 | 11##000000000000000##000000000 7 | 1100000000000000000##000000000 8 | 0000000000000000000##000000000 9 | 0P00000000000000000##00BB00000 10 | 1111100000000000000##001111111 11 | 1111100000BB0000000##001111111 12 | GGG11001111100000001100#####11 13 | GGG11001111100000001100#####11 14 | GGG11000###0000110000000####11 15 | GGG11000###000011000000000##11 16 | GGG11000###0000##00000000###11 17 | GGG11000###0000##00000000###11 18 | -------------------------------------------------------------------------------- /TinyLink/Assets/Rooms/7x1.txt: -------------------------------------------------------------------------------- 1 | 111111111111111111111111111111 2 | 111111111111111111111111111111 3 | 0000##############00#######000 4 | 0000############00000####00000 5 | 0000000####0000000000000000011 6 | 00000000###000000000000000S011 7 | 00000000###0000000000000011111 8 | 00000000#110000000000BB1111111 9 | 0P00000BB110000000b00111111111 10 | 11111111111GGGGGG1111111111111 11 | 11111111111GGGGGGG111111111111 12 | 11GGGGGGGGGGGGGGGGGG1111GGGGGG 13 | 11GGGGGGGGGGGGGGGGGGGGGGGGGGGG 14 | 11GGGGGGGGGGGGGGGGGGGGGGGGGGGG 15 | 11GGGGGGGGGGGGGGGGGGGGGGGGGGGG 16 | 11GGGGGGGGGGGGGGGGGGGGGGGGGGGG 17 | 11GGGGGGGGGGGGGGGGGGGGGGGGGGGG 18 | -------------------------------------------------------------------------------- /TinyLink/Assets/Rooms/8x0.txt: -------------------------------------------------------------------------------- 1 | 000000000000000000000000000000 2 | 00M000000000000000000000000000 3 | 00000000000000000000000000M000 4 | 000000000000000000000000000000 5 | 000000000000000000000000000000 6 | 000000M00000000000000000000000 7 | 000000000000000000000000000000 8 | 000000000000000000000000000000 9 | 000000000000000000000000M00000 10 | 00000000000###0000000000000000 11 | 00000000000####0000##000000000 12 | 00000000000####0000##000000000 13 | 00000###000######gg##g##00##00 14 | 000######11111111GG11111111111 15 | gP#######1111111GGGG1111111111 16 | 111----1111GGGGGGGGGGGGGGGGGGG 17 | 11100001111GGGGGGGGGGGGGGGGGGG 18 | -------------------------------------------------------------------------------- /TinyLink/Assets/Rooms/8x1.txt: -------------------------------------------------------------------------------- 1 | 111000011111111111111111111111 2 | 1###0BB11111111111111111111111 3 | 00###--1111GGGGGGGGGGGGGGGGGGG 4 | 0P0##00111GGGGGGGGGGGGGGGGGGGG 5 | 111111111GGGGGGGGGGGGGGGGGGGGG 6 | 111111111GGGGGGGGGGGGGGGGGGGGG 7 | 11111GGGGGGGGGGGGGGGGGGGGGGGGG 8 | GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG 9 | GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG 10 | GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG 11 | GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG 12 | GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG 13 | GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG 14 | GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG 15 | GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG 16 | GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG 17 | GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG 18 | -------------------------------------------------------------------------------- /TinyLink/Assets/Rooms/9x0.txt: -------------------------------------------------------------------------------- 1 | 000000000000000000000000000000 2 | 000000000000000000000000000000 3 | 000000000000000000000000000000 4 | 000000000000000000000000000000 5 | 000000000000000000000000000000 6 | 00000000000000000000000BBB##00 7 | 00000000000000000000000111##00 8 | 00000000000000000000000111#000 9 | 000000000000000000000001110000 10 | 00000000000000000M0000#1110000 11 | 00000##00000000000000##111#000 12 | 0000###000000000##00M######000 13 | 0P00###g0bgg000###BBB###D##000 14 | 1111111GGGGGGGGG11111111111111 15 | 1111111GGGGGGGGGG1111111111111 16 | GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG 17 | GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG 18 | -------------------------------------------------------------------------------- /TinyLink/Assets/Sprites/blob.ase: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FosterFramework/Samples/5b97ca5329e61768f85a45d655da5df7f882519d/TinyLink/Assets/Sprites/blob.ase -------------------------------------------------------------------------------- /TinyLink/Assets/Sprites/bramble.ase: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FosterFramework/Samples/5b97ca5329e61768f85a45d655da5df7f882519d/TinyLink/Assets/Sprites/bramble.ase -------------------------------------------------------------------------------- /TinyLink/Assets/Sprites/bullet.ase: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FosterFramework/Samples/5b97ca5329e61768f85a45d655da5df7f882519d/TinyLink/Assets/Sprites/bullet.ase -------------------------------------------------------------------------------- /TinyLink/Assets/Sprites/buttons.ase: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FosterFramework/Samples/5b97ca5329e61768f85a45d655da5df7f882519d/TinyLink/Assets/Sprites/buttons.ase -------------------------------------------------------------------------------- /TinyLink/Assets/Sprites/circle.ase: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FosterFramework/Samples/5b97ca5329e61768f85a45d655da5df7f882519d/TinyLink/Assets/Sprites/circle.ase -------------------------------------------------------------------------------- /TinyLink/Assets/Sprites/door.ase: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FosterFramework/Samples/5b97ca5329e61768f85a45d655da5df7f882519d/TinyLink/Assets/Sprites/door.ase -------------------------------------------------------------------------------- /TinyLink/Assets/Sprites/ghostfrog.ase: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FosterFramework/Samples/5b97ca5329e61768f85a45d655da5df7f882519d/TinyLink/Assets/Sprites/ghostfrog.ase -------------------------------------------------------------------------------- /TinyLink/Assets/Sprites/heart.ase: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FosterFramework/Samples/5b97ca5329e61768f85a45d655da5df7f882519d/TinyLink/Assets/Sprites/heart.ase -------------------------------------------------------------------------------- /TinyLink/Assets/Sprites/jumpthru.ase: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FosterFramework/Samples/5b97ca5329e61768f85a45d655da5df7f882519d/TinyLink/Assets/Sprites/jumpthru.ase -------------------------------------------------------------------------------- /TinyLink/Assets/Sprites/mosquito.ase: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FosterFramework/Samples/5b97ca5329e61768f85a45d655da5df7f882519d/TinyLink/Assets/Sprites/mosquito.ase -------------------------------------------------------------------------------- /TinyLink/Assets/Sprites/player.ase: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FosterFramework/Samples/5b97ca5329e61768f85a45d655da5df7f882519d/TinyLink/Assets/Sprites/player.ase -------------------------------------------------------------------------------- /TinyLink/Assets/Sprites/pop.ase: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FosterFramework/Samples/5b97ca5329e61768f85a45d655da5df7f882519d/TinyLink/Assets/Sprites/pop.ase -------------------------------------------------------------------------------- /TinyLink/Assets/Sprites/spitter.ase: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FosterFramework/Samples/5b97ca5329e61768f85a45d655da5df7f882519d/TinyLink/Assets/Sprites/spitter.ase -------------------------------------------------------------------------------- /TinyLink/Assets/Tilesets/back.ase: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FosterFramework/Samples/5b97ca5329e61768f85a45d655da5df7f882519d/TinyLink/Assets/Tilesets/back.ase -------------------------------------------------------------------------------- /TinyLink/Assets/Tilesets/castle.ase: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FosterFramework/Samples/5b97ca5329e61768f85a45d655da5df7f882519d/TinyLink/Assets/Tilesets/castle.ase -------------------------------------------------------------------------------- /TinyLink/Assets/Tilesets/grass.ase: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FosterFramework/Samples/5b97ca5329e61768f85a45d655da5df7f882519d/TinyLink/Assets/Tilesets/grass.ase -------------------------------------------------------------------------------- /TinyLink/Assets/Tilesets/plants.ase: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FosterFramework/Samples/5b97ca5329e61768f85a45d655da5df7f882519d/TinyLink/Assets/Tilesets/plants.ase -------------------------------------------------------------------------------- /TinyLink/Assets/Tilesets/water.ase: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FosterFramework/Samples/5b97ca5329e61768f85a45d655da5df7f882519d/TinyLink/Assets/Tilesets/water.ase -------------------------------------------------------------------------------- /TinyLink/README.md: -------------------------------------------------------------------------------- 1 | # Tiny Link 2 | ![Screenshot](screenshot.png "Screenshot") 3 | 4 | This is an example of a simple retro Platformer with a small tile-based Level Editor. For the sake of simplicity, this does not use a true ECS but rather a simple Actor inheritance system. For larger projects a proper ECS-like system is probably a good idea. 5 | 6 | Sprite Resources are created in [Aseprite](https://www.aseprite.org/), so you will need that if you wish to modify the graphics. The Aseprite Tag feature was used to dictate Animations within a single Sprite. 7 | 8 | This was originally a project created in 15 hours for a live stream. The original [can be found here](https://github.com/NoelFB/tiny_link). 9 | 10 | -------------------------------------------------------------------------------- /TinyLink/Source/Actors/Actor.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Numerics; 3 | using Foster.Framework; 4 | 5 | namespace TinyLink; 6 | 7 | public class Actor 8 | { 9 | [Flags] 10 | public enum Masks 11 | { 12 | None = 0, 13 | Solid = 1 << 0, 14 | Jumpthru = 1 << 1, 15 | Player = 1 << 2, 16 | Enemy = 1 << 3, 17 | Hazard = 1 << 4, 18 | } 19 | 20 | public Game Game = null!; 21 | public Point2 Position; 22 | public Vector2 Velocity; 23 | public Hitbox Hitbox; 24 | public Masks Mask = Masks.None; 25 | public Facing Facing = Facing.Right; 26 | public Vector2 Squish = Vector2.One; 27 | public Vector2 Shift = Vector2.Zero; 28 | public int Depth = 0; 29 | public float Timer = 0; 30 | public bool Visible = true; 31 | public float IFrameTime = 0.50f; 32 | public bool CollidesWithSolids = true; 33 | 34 | public Time Time => Game.Time; 35 | 36 | public Sprite? Sprite 37 | { 38 | get => sprite; 39 | set 40 | { 41 | if (sprite != value) 42 | { 43 | sprite = value; 44 | animation = value?.Animations[0] ?? new(); 45 | animationTime = 0; 46 | } 47 | } 48 | } 49 | 50 | public Sprite.Animation Animation => animation; 51 | public float AnimationTime => animationTime; 52 | 53 | private Vector2 remainder; 54 | private Sprite? sprite; 55 | private Sprite.Animation animation; 56 | private float animationTime = 0; 57 | private bool animationLooping = true; 58 | private float hitCooldown = 0; 59 | 60 | /// 61 | /// Plays an Animation from our current Sprite, if it exists 62 | /// 63 | public void Play(string name, bool looping = true, bool restart = false) 64 | { 65 | if (sprite != null && sprite.GetAnimation(name) is {} anim && (restart || animation.Name != name)) 66 | { 67 | animation = anim; 68 | animationTime = 0; 69 | } 70 | 71 | animationLooping = looping; 72 | } 73 | 74 | /// 75 | /// Checks if an animation is playing 76 | /// 77 | public bool IsPlaying(string name) 78 | => Animation.Name == name; 79 | 80 | /// 81 | /// Checks if an animation is finished playing. 82 | /// 83 | public bool IsFinishedPlaying(string? name = null) 84 | => !animationLooping && animationTime >= Animation.Duration && (name == null || IsPlaying(name)); 85 | 86 | /// 87 | /// Checks to see if we overlap any actors of the given mask 88 | /// 89 | public bool OverlapsAny(Masks mask) 90 | => OverlapsAny(Point2.Zero, mask); 91 | 92 | /// 93 | /// Checks to see if we overlap any actors of the given mask 94 | /// 95 | public bool OverlapsAny(Point2 offset, Masks mask) 96 | { 97 | foreach (var other in Game.Actors) 98 | { 99 | if (other != this && other.Mask.Has(mask) && OverlapsAny(offset, other)) 100 | return true; 101 | } 102 | 103 | return false; 104 | } 105 | 106 | /// 107 | /// Checks to see if we overlap the given actor 108 | /// 109 | public bool OverlapsAny(Point2 offset, Actor other) 110 | => Hitbox.Overlaps(Position + offset - other.Position, other.Hitbox); 111 | 112 | /// 113 | /// Finds the first overlapping actor of the given mask 114 | /// 115 | public Actor? OverlapsFirst(Point2 offset, Masks mask) 116 | { 117 | foreach (var other in Game.Actors) 118 | { 119 | if (other != this && other.Mask.Has(mask) && OverlapsAny(offset, other)) 120 | return other; 121 | } 122 | 123 | return null; 124 | } 125 | 126 | /// 127 | /// Finds the first overlapping actor of the given mask 128 | /// 129 | public Actor? OverlapsFirst(Masks mask) 130 | => OverlapsFirst(Point2.Zero, mask); 131 | 132 | /// 133 | /// Checks if we're on the Ground 134 | /// 135 | public bool Grounded() 136 | { 137 | foreach (var other in Game.Actors) 138 | { 139 | if (other == this || !other.Mask.Has(Masks.Solid | Masks.Jumpthru)) 140 | continue; 141 | 142 | if (!OverlapsAny(Point2.Down, other)) 143 | continue; 144 | 145 | if (other.Mask.Has(Masks.Jumpthru) && OverlapsAny(Point2.Zero, other)) 146 | continue; 147 | 148 | return true; 149 | } 150 | 151 | return false; 152 | } 153 | 154 | /// 155 | /// Moves a single Pixel 156 | /// 157 | public bool MovePixel(Point2 sign) 158 | { 159 | sign.X = Math.Sign(sign.X); 160 | sign.Y = Math.Sign(sign.Y); 161 | 162 | if (CollidesWithSolids) 163 | { 164 | if (OverlapsAny(sign, Masks.Solid)) 165 | return false; 166 | 167 | if (sign.Y > 0 && Grounded()) 168 | return false; 169 | } 170 | 171 | Position += sign; 172 | return true; 173 | } 174 | 175 | /// 176 | /// Moves a floating value, which increments an accumulator and only moves in pixel values 177 | /// 178 | public void Move(Vector2 value) 179 | { 180 | remainder += value; 181 | Point2 move = (Point2)remainder; 182 | remainder -= move; 183 | 184 | while (move.X != 0) 185 | { 186 | var sign = Math.Sign(move.X); 187 | if (!MovePixel(Point2.UnitX * sign)) 188 | { 189 | OnCollideX(); 190 | break; 191 | } 192 | else 193 | { 194 | move.X -= sign; 195 | } 196 | } 197 | 198 | while (move.Y != 0) 199 | { 200 | var sign = Math.Sign(move.Y); 201 | if (!MovePixel(Point2.UnitY * sign)) 202 | { 203 | OnCollideY(); 204 | break; 205 | } 206 | else 207 | { 208 | move.Y -= sign; 209 | } 210 | } 211 | } 212 | 213 | /// 214 | /// Stops all velocity 215 | /// 216 | public void Stop() 217 | { 218 | Velocity = Vector2.Zero; 219 | remainder = Vector2.Zero; 220 | } 221 | 222 | /// 223 | /// Stops X Velocity 224 | /// 225 | public void StopX() 226 | { 227 | Velocity.X = 0; 228 | remainder.X = 0; 229 | } 230 | 231 | /// 232 | /// Stops Y Velocity 233 | /// 234 | public void StopY() 235 | { 236 | Velocity.Y = 0; 237 | remainder.Y = 0; 238 | } 239 | 240 | /// 241 | /// Tries to Hit another Actor. 242 | /// What a "Hit" is, is up to the Actors 243 | /// 244 | public bool Hit(Actor actor) 245 | { 246 | if (actor.hitCooldown <= 0) 247 | { 248 | actor.hitCooldown = actor.IFrameTime; 249 | actor.OnWasHit(this); 250 | OnPerformHit(actor); 251 | return true; 252 | } 253 | 254 | return false; 255 | } 256 | 257 | /// 258 | /// Called when the Actor is added to the Game 259 | /// 260 | public virtual void Added() { } 261 | 262 | /// 263 | /// What to do when we collide with a wall while moving horizontally 264 | /// 265 | public virtual void OnCollideX() => StopX(); 266 | 267 | /// 268 | /// What to do when we collide with a wall while moving vertically 269 | /// 270 | public virtual void OnCollideY() => StopY(); 271 | 272 | /// 273 | /// Called when we were hit by another actor 274 | /// 275 | public virtual void OnWasHit(Actor by) { } 276 | 277 | /// 278 | /// Called when we perform a hit on another actor 279 | /// 280 | public virtual void OnPerformHit(Actor hitting) { } 281 | 282 | /// 283 | /// Called ones per frame, updates our Sprite and Timer 284 | /// 285 | public virtual void Update() 286 | { 287 | if (Velocity != Vector2.Zero) 288 | Move(Velocity * Time.Delta); 289 | Squish = Calc.Approach(Squish, Vector2.One, Time.Delta * 4.0f); 290 | hitCooldown = Calc.Approach(hitCooldown, 0, Time.Delta); 291 | animationTime += Time.Delta; 292 | Timer += Time.Delta; 293 | } 294 | 295 | /// 296 | /// Draws the current Sprite 297 | /// 298 | public virtual void Render(Batcher batcher) 299 | { 300 | if (hitCooldown > 0 && Time.BetweenInterval(0.05f)) 301 | return; 302 | 303 | if (sprite != null) 304 | { 305 | var frame = sprite.GetFrameAt(animation, animationTime, animationLooping); 306 | batcher.PushMatrix(Matrix3x2.CreateScale(Facing * Squish.X, Squish.Y)); 307 | batcher.Image(frame.Subtexture, Shift, sprite.Origin, Vector2.One, 0, Color.White); 308 | batcher.PopMatrix(); 309 | } 310 | } 311 | 312 | /// 313 | /// Called when the Actor was destroyed 314 | /// 315 | public virtual void Destroyed() { } 316 | } -------------------------------------------------------------------------------- /TinyLink/Source/Actors/Blob.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | using Foster.Framework; 3 | 4 | namespace TinyLink; 5 | 6 | public class Blob : Actor 7 | { 8 | public int Health = 3; 9 | public float JumpTimer = 2; 10 | private bool grounded = true; 11 | 12 | public Blob() 13 | { 14 | Sprite = Assets.GetSprite("blob"); 15 | Hitbox = new(new RectInt(-4, -8, 8, 8)); 16 | Mask = Masks.Enemy; 17 | Depth = -5; 18 | Play("fly"); 19 | } 20 | 21 | public override void Update() 22 | { 23 | base.Update(); 24 | 25 | Velocity.Y += 300 * Time.Delta; 26 | 27 | if (Grounded()) 28 | { 29 | if (!grounded) 30 | { 31 | Play("idle"); 32 | Squish = new Vector2(1.5f, 0.50f); 33 | } 34 | 35 | Velocity.X = Calc.Approach(Velocity.X, 0, 400 * Time.Delta); 36 | JumpTimer -= Time.Delta; 37 | grounded = true; 38 | } 39 | else 40 | { 41 | grounded = false; 42 | } 43 | 44 | if (JumpTimer <= 0) 45 | { 46 | Play("jump"); 47 | JumpTimer = 2; 48 | Velocity.Y = -90; 49 | Squish = new Vector2(0.5f, 1.5f); 50 | 51 | if (Game.GetFirst(Masks.Player) is Actor player) 52 | { 53 | var dir = MathF.Sign(player.Position.X - Position.X); 54 | if (dir == 0) dir = 1; 55 | 56 | Facing = dir; 57 | Velocity.X = Facing * 40; 58 | } 59 | } 60 | } 61 | 62 | public override void OnWasHit(Actor by) 63 | { 64 | Health--; 65 | 66 | if (Health <= 0) 67 | { 68 | Game.Create(Position + new Point2(0, -4)); 69 | Game.Destroy(this); 70 | } 71 | else 72 | { 73 | var sign = MathF.Sign(Position.X - by.Position.X); 74 | Velocity.X = sign * 120; 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /TinyLink/Source/Actors/Bramble.cs: -------------------------------------------------------------------------------- 1 | using Foster.Framework; 2 | 3 | namespace TinyLink; 4 | 5 | public class Bramble : Actor 6 | { 7 | public Bramble() 8 | { 9 | Hitbox = new(new RectInt(-4, -8, 8, 8)); 10 | Mask = Masks.Hazard; 11 | Sprite = Assets.GetSprite("bramble"); 12 | Play("idle"); 13 | } 14 | 15 | public override void OnPerformHit(Actor hitting) => Pop(); 16 | public override void OnWasHit(Actor by) => Pop(); 17 | 18 | public void Pop() 19 | { 20 | Game.Destroy(this); 21 | Game.Create(Position + new Point2(0, -4)); 22 | } 23 | } -------------------------------------------------------------------------------- /TinyLink/Source/Actors/Bullet.cs: -------------------------------------------------------------------------------- 1 | using Foster.Framework; 2 | 3 | namespace TinyLink; 4 | 5 | public class Bullet : Actor 6 | { 7 | public const float Gravity = 130; 8 | 9 | public Bullet() 10 | { 11 | Hitbox = new(new RectInt(-4, -4, 8, 8)); 12 | Mask = Masks.Enemy; 13 | Sprite = Assets.GetSprite("bullet"); 14 | Depth = -5; 15 | } 16 | 17 | public override void Update() 18 | { 19 | base.Update(); 20 | 21 | Velocity.Y += Gravity * Time.Delta; 22 | 23 | if (Timer > 2.5f && Time.BetweenInterval(0.05f)) 24 | Visible = !Visible; 25 | 26 | if (Timer > 3.0f) 27 | Game.Destroy(this); 28 | } 29 | 30 | public override void OnCollideX() => Game.Destroy(this); 31 | public override void OnCollideY() => Velocity.Y = -60; 32 | 33 | public override void OnPerformHit(Actor hitting) => Pop(); 34 | public override void OnWasHit(Actor by) => Pop(); 35 | 36 | public void Pop() 37 | { 38 | Game.Hitstun(0.1f); 39 | Game.Destroy(this); 40 | Game.Create(Position); 41 | } 42 | } -------------------------------------------------------------------------------- /TinyLink/Source/Actors/Door.cs: -------------------------------------------------------------------------------- 1 | using Foster.Framework; 2 | 3 | namespace TinyLink; 4 | 5 | public class Door : Actor 6 | { 7 | private float delay = 0; 8 | 9 | public Door() 10 | { 11 | Sprite = Assets.GetSprite("door"); 12 | Depth = -1; 13 | Visible = false; 14 | Play("idle"); 15 | } 16 | 17 | public void Appear() 18 | { 19 | Hitbox = new (new RectInt(-6, -16, 12, 16)); 20 | Mask = Masks.Solid; 21 | Visible = true; 22 | 23 | if (Timer > 0.1f) 24 | Game.Create(Position + new Point2(0, -8)); 25 | } 26 | 27 | public override void Update() 28 | { 29 | base.Update(); 30 | 31 | // wait to appear 32 | if (!Visible) 33 | { 34 | if (Game.GetFirst(Masks.Player) is not Actor player || player.Position.X > Position.X + 12) 35 | Appear(); 36 | } 37 | // wait for all enemies to be gone 38 | else if (Timer > 0.25f) 39 | { 40 | bool anyEnemiesAlive = false; 41 | 42 | foreach (var it in Game.Actors) 43 | if (it.Mask == Masks.Enemy) 44 | { 45 | anyEnemiesAlive = true; 46 | break; 47 | } 48 | 49 | if (!anyEnemiesAlive) 50 | { 51 | if (delay > 0.50f) 52 | { 53 | Game.Create(Position + new Point2(0, -8)); 54 | Game.Destroy(this); 55 | } 56 | delay += Time.Delta; 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /TinyLink/Source/Actors/GhostFrog.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | using Foster.Framework; 3 | 4 | namespace TinyLink; 5 | 6 | public class GhostFrog : Actor 7 | { 8 | private enum States 9 | { 10 | Waiting, 11 | ReadyingAttack, 12 | PerformSlash, 13 | Floating, 14 | Shoot, 15 | Reflect, 16 | Dead 17 | } 18 | 19 | private const int MaxHealthPhase1 = 10; 20 | private const int MaxHealthPhase2 = 3; 21 | 22 | private States state = States.Waiting; 23 | private int health = MaxHealthPhase1; 24 | private int phase = 0; 25 | private int side = 1; 26 | private int reflect_count = 0; 27 | private float friction = 0; 28 | private Point2 home; 29 | private Point2 lastPosition; 30 | private Point2 playerPosition; 31 | private IEnumerator? routine; 32 | private float routineWait; 33 | private readonly Func>[] stateRoutines; 34 | private Rng rng; 35 | 36 | public GhostFrog() 37 | { 38 | Sprite = Assets.GetSprite("ghostfrog"); 39 | Depth = -5; 40 | Hitbox = new(new RectInt(-4, -12, 8, 12)); 41 | Mask = Masks.Enemy; 42 | rng = new(Guid.NewGuid().Variant); 43 | Play("sword"); 44 | 45 | stateRoutines = new[] 46 | { 47 | WaitingRoutine, 48 | ReadyingAttackRoutine, 49 | PerformSlashRoutine, 50 | FloatingRoutine, 51 | ShootRoutine, 52 | ReflectRoutine, 53 | DeadRoutine, 54 | }; 55 | 56 | SetState(States.Waiting); 57 | } 58 | 59 | public override void Added() 60 | { 61 | base.Added(); 62 | home = Position; 63 | } 64 | 65 | private IEnumerator WaitingRoutine() 66 | { 67 | while (true) 68 | yield return 0; 69 | } 70 | 71 | private IEnumerator ReadyingAttackRoutine() 72 | { 73 | float stateTimer = 0; 74 | 75 | while (true) 76 | { 77 | Facing = MathF.Sign(playerPosition.X - Position.X); 78 | if (Facing == 0) 79 | Facing = 1; 80 | 81 | float targetX = playerPosition.X + 32 * -Facing; 82 | Velocity.X = Calc.Approach(Velocity.X, MathF.Sign(targetX - Position.X) * 40, 400 * Time.Delta); 83 | friction = 100; 84 | Play("run"); 85 | 86 | if (stateTimer > 3.0f || (stateTimer > 1.0f && OverlapsAny(new Point2(-Facing * 8, 0), Masks.Solid))) 87 | { 88 | Velocity.X = 0; 89 | SetState(States.PerformSlash); 90 | } 91 | 92 | stateTimer += Time.Delta; 93 | yield return 0; 94 | } 95 | } 96 | 97 | private IEnumerator PerformSlashRoutine() 98 | { 99 | // start attack anim 100 | Play("attack", false); 101 | friction = 500; 102 | 103 | // after 0.8s, do the lunge 104 | yield return 0.80f; 105 | 106 | Velocity.X = Facing * 250; 107 | Hitbox = new(new RectInt(-4 + Facing * 4, -12, 8, 12)); 108 | 109 | while (!IsFinishedPlaying("attack")) 110 | { 111 | RectInt rect = new(8, -8, 20, 8); 112 | if (Facing < 0) 113 | rect.X = -(rect.X + rect.Width); 114 | 115 | if (Game.OverlapsFirst(Position + rect, Masks.Player) is Actor hit) 116 | Hit(hit); 117 | 118 | yield return 0; 119 | } 120 | 121 | Hitbox = new(new RectInt(-4, -12, 8, 12)); 122 | if (health > 0) 123 | { 124 | SetState(States.ReadyingAttack); 125 | } 126 | else 127 | { 128 | phase = 1; 129 | health = MaxHealthPhase2; 130 | side = rng.Chance(0.50f) ? -1 : 1; 131 | SetState(States.Floating); 132 | } 133 | } 134 | 135 | private IEnumerator FloatingRoutine() 136 | { 137 | float stateTimer = 0; 138 | while (stateTimer < 5.0f) 139 | { 140 | Play("float"); 141 | 142 | friction = 0; 143 | CollidesWithSolids = false; 144 | 145 | int targetY = home.Y - 50; 146 | int targetX = home.X + side * 50; 147 | 148 | if (MathF.Sign(targetY - Position.Y) != MathF.Sign(targetY - lastPosition.Y)) 149 | { 150 | Velocity.Y = 0; 151 | Position.Y = targetY; 152 | } 153 | else 154 | Velocity.Y = Calc.Approach(Velocity.Y, MathF.Sign(targetY - Position.Y) * 50, 800 * Time.Delta); 155 | 156 | if (MathF.Abs(Position.Y - targetY) < 8) 157 | Velocity.X = Calc.Approach(Velocity.X, MathF.Sign(targetX - Position.X) * 120, 800 * Time.Delta); 158 | else 159 | Velocity.X = 0; 160 | 161 | if (MathF.Abs(targetX - Position.X) < 8 && MathF.Abs(targetY - Position.Y) < 8) 162 | break; 163 | 164 | stateTimer += Time.Delta; 165 | yield return 0; 166 | } 167 | 168 | SetState(States.Shoot); 169 | } 170 | 171 | private IEnumerator ShootRoutine() 172 | { 173 | float time = 0.0f; 174 | while (time < 1.0f) 175 | { 176 | Velocity = Calc.Approach(Velocity, Vector2.Zero, 300 * Time.Delta); 177 | 178 | Facing = MathF.Sign(playerPosition.X - Position.X); 179 | if (Facing == 0) 180 | Facing = 1; 181 | time += Time.Delta; 182 | yield return 0; 183 | } 184 | 185 | Play("reflect"); 186 | yield return 0.20f; 187 | 188 | reflect_count = 0; 189 | Game.Create(Position + new Point2(Facing * 12, -8)); 190 | yield return 0.20f; 191 | 192 | Play("float"); 193 | SetState(States.Reflect); 194 | } 195 | 196 | private IEnumerator ReflectRoutine() 197 | { 198 | while (true) 199 | { 200 | var orb = Game.GetFirst(); 201 | if (orb == null) 202 | break; 203 | 204 | // wait until orb is coming towards us 205 | if (orb.TowardsPlayer) 206 | { 207 | yield return 0; 208 | continue; 209 | } 210 | 211 | var distance = (orb.Position - orb.Target).Length(); 212 | 213 | if (reflect_count < 2) 214 | { 215 | if (distance < 20) 216 | { 217 | var sign = MathF.Sign(orb.Position.X - Position.X); 218 | if (sign != 0) 219 | Facing = sign; 220 | Play("reflect", false); 221 | 222 | var was = orb.Speed; 223 | orb.Speed = 0; 224 | yield return 0.1f; 225 | 226 | orb.Speed = was; 227 | Hit(orb); 228 | reflect_count++; 229 | yield return 0.4f; 230 | 231 | Play("float"); 232 | continue; 233 | } 234 | } 235 | else 236 | { 237 | if (distance < 8) 238 | orb.Hit(this); 239 | } 240 | 241 | yield return 0; 242 | } 243 | 244 | yield return 1.0f; 245 | side = -side; 246 | SetState(States.Floating); 247 | } 248 | 249 | private IEnumerator DeadRoutine() 250 | { 251 | float stateTimer = 0; 252 | while (stateTimer < 3.0f) 253 | { 254 | Play("dead"); 255 | Game.Shake(1.0f); 256 | 257 | if (Time.OnInterval(0.25f)) 258 | { 259 | var offset = new Point2(rng.Int(-16, 16), rng.Int(-16, 16)); 260 | Game.Create(Position + new Point2(0, -8) + offset); 261 | } 262 | 263 | stateTimer += Time.Delta; 264 | yield return 0; 265 | } 266 | 267 | for (int x = -1; x < 2; x ++) 268 | for (int y = -1; y < 2; y ++) 269 | Game.Create(Position + new Point2(x * 12, -8 + y * 12)); 270 | 271 | Game.Hitstun(0.3f); 272 | Game.Shake(0.1f); 273 | Game.Destroy(this); 274 | } 275 | 276 | public override void Update() 277 | { 278 | base.Update(); 279 | 280 | var player = Game.GetFirst(Masks.Player); 281 | if (player != null) 282 | playerPosition = player.Position; 283 | 284 | if (friction > 0 && CollidesWithSolids && Grounded()) 285 | Velocity.X = Calc.Approach(Velocity.X, 0, friction * Time.Delta); 286 | 287 | // run our routine 288 | if (routine != null) 289 | { 290 | var was = routine; 291 | if (routineWait > 0) 292 | routineWait -= Time.Delta; 293 | else if (routine.MoveNext()) 294 | routineWait = routine.Current; 295 | else if (was == routine) 296 | routine = null; 297 | } 298 | 299 | // floaty visual behavior 300 | if (state == States.Floating || state == States.Shoot || state == States.Reflect) 301 | Shift.Y = MathF.Sin(Timer * 2) * 3; 302 | else 303 | Shift.Y = 0; 304 | 305 | lastPosition = Position; 306 | } 307 | 308 | private void SetState(States nextState) 309 | { 310 | state = nextState; 311 | routineWait = 0; 312 | routine = stateRoutines[(int)state](); 313 | } 314 | 315 | public override void OnWasHit(Actor by) 316 | { 317 | if (health > 0) 318 | { 319 | health--; 320 | if (health <= 0 && phase > 0) 321 | SetState(States.Dead); 322 | 323 | if (state == States.Waiting) 324 | { 325 | Game.Create(Position + new Point2(0, -8)); 326 | Game.Hitstun(0.25f); 327 | SetState(States.ReadyingAttack); 328 | } 329 | } 330 | } 331 | } 332 | -------------------------------------------------------------------------------- /TinyLink/Source/Actors/Grid.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | using Foster.Framework; 3 | 4 | namespace TinyLink; 5 | 6 | public class Grid : Actor 7 | { 8 | private readonly bool[,] grid = new bool[Game.Columns, Game.Rows]; 9 | private readonly Subtexture[,] tilemap = new Subtexture[Game.Columns, Game.Rows]; 10 | 11 | public Grid() 12 | { 13 | Hitbox = new(grid); 14 | } 15 | 16 | public void Set(int x, int y, Subtexture subtexture) 17 | { 18 | tilemap[x, y] = subtexture; 19 | grid[x, y] = true; 20 | } 21 | 22 | public override void Render(Batcher batcher) 23 | { 24 | for (int x = 0; x < Game.Columns; x ++) 25 | for (int y = 0; y < Game.Rows; y ++) 26 | { 27 | if (grid[x, y]) 28 | batcher.Image(tilemap[x, y], new Vector2(x, y) * Game.TileSize, Color.White); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /TinyLink/Source/Actors/Jumpthru.cs: -------------------------------------------------------------------------------- 1 | using Foster.Framework; 2 | 3 | namespace TinyLink; 4 | 5 | public class Jumpthru : Actor 6 | { 7 | public Jumpthru() 8 | { 9 | Hitbox = new(new RectInt(0, 0, Game.TileSize, Game.TileSize / 4)); 10 | Mask = Actor.Masks.Jumpthru; 11 | Sprite = Assets.GetSprite("jumpthru"); 12 | Depth = 5; 13 | Play("idle"); 14 | } 15 | } -------------------------------------------------------------------------------- /TinyLink/Source/Actors/Mosquito.cs: -------------------------------------------------------------------------------- 1 | using Foster.Framework; 2 | 3 | namespace TinyLink; 4 | 5 | public class Mosquito : Actor 6 | { 7 | public int Health = 2; 8 | 9 | public Mosquito() 10 | { 11 | Sprite = Assets.GetSprite("mosquito"); 12 | Hitbox = new(new RectInt(-5, -5, 10, 10)); 13 | Mask = Masks.Enemy; 14 | Depth = -5; 15 | CollidesWithSolids = false; 16 | Play("fly"); 17 | } 18 | 19 | public override void Update() 20 | { 21 | base.Update(); 22 | 23 | if (Game.GetFirst(Masks.Player) is Actor player) 24 | { 25 | var diff = player.Position.X - Position.X; 26 | var dist = MathF.Abs(diff); 27 | var sign = MathF.Sign(diff); 28 | 29 | if (dist < 100) 30 | Velocity.X += sign * 100 * Time.Delta; 31 | else 32 | Velocity.X = Calc.Approach(Velocity.X, 0, 100 * Time.Delta); 33 | 34 | if (MathF.Abs(Velocity.X) > 50) 35 | Velocity.X = Calc.Approach(Velocity.X, MathF.Sign(Velocity.X) * 50, 800 * Time.Delta); 36 | } 37 | 38 | Velocity.Y = MathF.Sin(Timer * 4) * 10; 39 | } 40 | 41 | public override void OnWasHit(Actor by) 42 | { 43 | Health--; 44 | 45 | if (Health <= 0) 46 | { 47 | Game.Create(Position); 48 | Game.Destroy(this); 49 | } 50 | else 51 | { 52 | var sign = MathF.Sign(Position.X - by.Position.X); 53 | Velocity.X = sign * 140; 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /TinyLink/Source/Actors/Orb.cs: -------------------------------------------------------------------------------- 1 | using Foster.Framework; 2 | 3 | namespace TinyLink; 4 | 5 | public class Orb : Actor 6 | { 7 | public bool TowardsPlayer = true; 8 | public float Speed = 40; 9 | 10 | public Orb() 11 | { 12 | Hitbox = new(new RectInt(-4, -4, 8, 8)); 13 | Mask = Masks.Hazard; 14 | Sprite = Assets.GetSprite("bullet"); 15 | Depth = -5; 16 | } 17 | 18 | public Point2 Target 19 | { 20 | get 21 | { 22 | var player = Game.GetFirst(Masks.Player); 23 | var enemy = Game.GetFirst(Masks.Enemy); 24 | 25 | if (player != null && enemy != null) 26 | return TowardsPlayer ? player.Position : enemy.Position + new Point2(0, -8); 27 | 28 | return Point2.Zero; 29 | } 30 | } 31 | 32 | public override void Update() 33 | { 34 | base.Update(); 35 | 36 | var diff = (Target - Position).Normalized(); 37 | Velocity = diff * Speed; 38 | } 39 | 40 | public override void Destroyed() 41 | { 42 | Game.Create(Position); 43 | } 44 | 45 | public override void OnWasHit(Actor by) 46 | { 47 | TowardsPlayer = !TowardsPlayer; 48 | Speed += 40; 49 | } 50 | 51 | public override void OnPerformHit(Actor hitting) 52 | { 53 | Game.Destroy(this); 54 | } 55 | } -------------------------------------------------------------------------------- /TinyLink/Source/Actors/Player.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | using Foster.Framework; 3 | 4 | namespace TinyLink; 5 | 6 | public class Player : Actor 7 | { 8 | public enum States 9 | { 10 | Normal, 11 | Attack, 12 | Hurt, 13 | Start 14 | } 15 | 16 | public const int MaxHealth = 4; 17 | private const float MaxGroundSpeed = 60; 18 | private const float MaxAirSpeed = 70; 19 | private const float GroundAccel = 500; 20 | private const float AirAccel = 100; 21 | private const float Friction = 800; 22 | private const float AttackFriction = 150; 23 | private const float HurtFriction = 200; 24 | private const float Gravity = 450; 25 | private const float JumpForce = -105; 26 | private const float JumpTime = 0.18f; 27 | private const float HurtDuration = 0.5f; 28 | private const float DeathDuration = 1.5f; 29 | private const float InvincibleDuration = 1.5f; 30 | 31 | public int Health = MaxHealth; 32 | public States State; 33 | public Controls Controls => Game.Controls; 34 | 35 | private float stateDuration = 0; 36 | private float jumpTimer = 0; 37 | private bool grounded = false; 38 | private bool ducking = false; 39 | 40 | public Player() 41 | { 42 | State = States.Start; 43 | Sprite = Assets.GetSprite("player"); 44 | Hitbox = new(new RectInt(-4, -12, 8, 12)); 45 | Mask = Masks.Player; 46 | IFrameTime = InvincibleDuration; 47 | grounded = true; 48 | Play("sword"); 49 | } 50 | 51 | public override void Update() 52 | { 53 | base.Update(); 54 | 55 | // update grounded state 56 | var nowGrounded = Velocity.Y >= 0 && Grounded(); 57 | if (nowGrounded && !grounded) 58 | Squish = new Vector2(1.5f, 0.70f); 59 | grounded = nowGrounded; 60 | 61 | // increment state timer 62 | var wasState = State; 63 | 64 | // state control 65 | switch (State) 66 | { 67 | case States.Normal: 68 | NormalState(); 69 | break; 70 | case States.Attack: 71 | AttackState(); 72 | break; 73 | case States.Hurt: 74 | HurtState(); 75 | break; 76 | case States.Start: 77 | StartState(); 78 | break; 79 | } 80 | 81 | // ducking collider(s) 82 | if (ducking && State != States.Normal) 83 | ducking = false; 84 | if (ducking) 85 | Hitbox = new(new RectInt(-4, -6, 8, 6)); 86 | else 87 | Hitbox = new(new RectInt(-4, -12, 8, 12)); 88 | 89 | // variable jumping 90 | if (jumpTimer > 0) 91 | { 92 | Velocity.Y = JumpForce; 93 | jumpTimer -= Time.Delta; 94 | if (!Controls.Jump.Down) 95 | jumpTimer = 0; 96 | } 97 | 98 | // gravity 99 | if (!grounded) 100 | { 101 | float grav = Gravity; 102 | if (State == States.Normal && MathF.Abs(Velocity.Y) < 20 && Controls.Jump.Down) 103 | grav *= 0.40f; 104 | Velocity.Y += grav * Time.Delta; 105 | } 106 | 107 | // goto next room 108 | if (Health > 0) 109 | { 110 | if (Position.X > Game.Bounds.Right && !Game.Transition(Point2.Right)) 111 | { 112 | Position.X = Game.Bounds.Right; 113 | } 114 | else if (Position.X < Game.Bounds.Left && !Game.Transition(Point2.Left)) 115 | { 116 | Position.X = Game.Bounds.Left; 117 | } 118 | else if (Position.Y > Game.Bounds.Bottom + 12 && !Game.Transition(Point2.Down)) 119 | { 120 | Health = 0; 121 | State = States.Hurt; 122 | } 123 | else if (Position.Y < Game.Bounds.Top) 124 | { 125 | if (Game.Transition(Point2.Up)) 126 | Velocity.Y = -150; 127 | else 128 | Position.Y = Game.Bounds.Top; 129 | } 130 | } 131 | 132 | // detect getting hit 133 | if (OverlapsFirst(Masks.Enemy | Masks.Hazard) is Actor hit) 134 | hit.Hit(this); 135 | 136 | stateDuration += Time.Delta; 137 | if (State != wasState) 138 | stateDuration = 0.0f; 139 | } 140 | 141 | public void NormalState() 142 | { 143 | // update ducking state 144 | ducking = grounded && Controls.Move.IntValue.Y > 0; 145 | 146 | // get input 147 | var input = Controls.Move.IntValue.X; 148 | if (ducking) 149 | input = 0; 150 | 151 | // sprite 152 | if (grounded) 153 | { 154 | if (ducking) 155 | Play("duck"); 156 | else if (input == 0) 157 | Play("idle"); 158 | else 159 | Play("run"); 160 | } 161 | else 162 | { 163 | Play("jump"); 164 | } 165 | 166 | // horizontal movement 167 | { 168 | // Acceleration 169 | Velocity.X += input * (grounded ? GroundAccel : AirAccel) * Time.Delta; 170 | 171 | // Max Speed 172 | var maxspd = grounded ? MaxGroundSpeed : MaxAirSpeed; 173 | if (MathF.Abs(Velocity.X) > maxspd) 174 | Velocity.X = Calc.Approach(Velocity.X, MathF.Sign(Velocity.X) * maxspd, 2000 * Time.Delta); 175 | 176 | // Friction 177 | if (input == 0 && grounded) 178 | Velocity.X = Calc.Approach(Velocity.X, 0, Friction * Time.Delta); 179 | 180 | // Facing 181 | if (grounded && input != 0) 182 | Facing = input; 183 | } 184 | 185 | // Start jumping 186 | if (grounded && Controls.Jump.ConsumePress()) 187 | { 188 | Squish = new Vector2(0.65f, 1.4f); 189 | StopX(); 190 | Velocity.X = input * MaxAirSpeed; 191 | jumpTimer = JumpTime; 192 | } 193 | 194 | // Begin Attack 195 | if (Controls.Attack.ConsumePress()) 196 | { 197 | State = States.Attack; 198 | if (grounded) 199 | StopX(); 200 | } 201 | } 202 | 203 | public void AttackState() 204 | { 205 | Play("attack", false); 206 | 207 | RectInt? hitbox = null; 208 | 209 | if (stateDuration < 0.2f) 210 | { 211 | hitbox = new RectInt(-16, -12, 17, 8); 212 | } 213 | else if (stateDuration < 0.50f) 214 | { 215 | hitbox = new RectInt(8, -8, 16, 8); 216 | } 217 | 218 | if (hitbox != null) 219 | { 220 | var it = hitbox.Value; 221 | if (Facing == Facing.Left) 222 | it.X = -(it.X + it.Width); 223 | it += Position; 224 | 225 | if (Game.OverlapsFirst(it, Masks.Enemy | Masks.Hazard) is Actor hit) 226 | Hit(hit); 227 | } 228 | 229 | if (Grounded()) 230 | Velocity.X = Calc.Approach(Velocity.X, 0, AttackFriction * Time.Delta); 231 | 232 | if (stateDuration >= Animation.Duration) 233 | { 234 | Play("idle"); 235 | State = States.Normal; 236 | } 237 | } 238 | 239 | public void HurtState() 240 | { 241 | if (stateDuration <= 0 && Health <= 0) 242 | { 243 | foreach (var actor in Game.Actors) 244 | if (actor != this) 245 | Game.Destroy(actor); 246 | Game.Shake(0.1f); 247 | } 248 | 249 | Velocity.X = Calc.Approach(Velocity.X, 0, HurtFriction * Time.Delta); 250 | 251 | if (stateDuration >= HurtDuration && Health > 0) 252 | State = States.Normal; 253 | 254 | if (stateDuration >= DeathDuration && Health <= 0) 255 | Game.ReloadRoom(); 256 | } 257 | 258 | public void StartState() 259 | { 260 | if (stateDuration >= 1.0f) 261 | State = States.Normal; 262 | } 263 | 264 | public override void OnWasHit(Actor by) 265 | { 266 | Game.Hitstun(0.1f); 267 | Game.Shake(0.1f); 268 | 269 | Play("hurt"); 270 | 271 | Velocity = new Vector2(-Facing * 100, -80); 272 | State = States.Hurt; 273 | Health--; 274 | } 275 | } -------------------------------------------------------------------------------- /TinyLink/Source/Actors/Pop.cs: -------------------------------------------------------------------------------- 1 | using Foster.Framework; 2 | 3 | namespace TinyLink; 4 | 5 | public class Pop : Actor 6 | { 7 | public Pop() 8 | { 9 | Sprite = Assets.GetSprite("pop"); 10 | Play("pop", false); 11 | Depth = -20; 12 | } 13 | 14 | public override void Update() 15 | { 16 | base.Update(); 17 | 18 | if (IsFinishedPlaying()) 19 | Game.Destroy(this); 20 | } 21 | } -------------------------------------------------------------------------------- /TinyLink/Source/Actors/Spitter.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | using Foster.Framework; 3 | 4 | namespace TinyLink; 5 | 6 | public class Spitter : Actor 7 | { 8 | private float timer = 0; 9 | 10 | public Spitter() 11 | { 12 | Hitbox = new(new RectInt(-6, -12, 12, 12)); 13 | Mask = Masks.Enemy; 14 | Sprite = Assets.GetSprite("spitter"); 15 | Depth = -5; 16 | timer = 1.0f; 17 | Play("idle"); 18 | } 19 | 20 | public override void Update() 21 | { 22 | base.Update(); 23 | 24 | timer -= Time.Delta; 25 | if (timer <= 0) 26 | { 27 | Play("shoot", false); 28 | timer = 3.0f; 29 | 30 | var bullet = Game.Create(Position + new Point2(-8, -8)); 31 | bullet.Velocity = new Vector2(-40, 0); 32 | } 33 | 34 | if (IsPlaying("shoot") && IsFinishedPlaying()) 35 | Play("idle"); 36 | } 37 | 38 | public override void OnWasHit(Actor by) 39 | { 40 | Game.Hitstun(0.1f); 41 | Game.Destroy(this); 42 | Game.Create(Position + new Point2(0, -4)); 43 | } 44 | } -------------------------------------------------------------------------------- /TinyLink/Source/Actors/TitleText.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | using Foster.Framework; 3 | 4 | namespace TinyLink; 5 | 6 | /// 7 | /// Uses the Room's Text Value to show text. 8 | /// 9 | public class TitleText : Actor 10 | { 11 | public override void Render(Batcher batcher) 12 | { 13 | if (Assets.Font != null && Game.CurrentRoom != null) 14 | { 15 | batcher.Text(Assets.Font, Game.CurrentRoom.Text, Vector2.Zero, Vector2.One * 0.50f, Color.White); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /TinyLink/Source/Assets/Assets.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Numerics; 3 | using Foster.Framework; 4 | 5 | namespace TinyLink; 6 | 7 | public static class Assets 8 | { 9 | public static SpriteFont? Font { get; private set; } 10 | public static Texture? Atlas { get; private set; } 11 | public static readonly Dictionary Sprites = new(); 12 | public static readonly Dictionary Tilesets = new(); 13 | public static readonly Dictionary Subtextures = new(); 14 | public static readonly Dictionary Rooms = new(); 15 | 16 | private const string assetsFolderName = "Assets"; 17 | private static string? path = null; 18 | 19 | public static string AssetsPath 20 | { 21 | get 22 | { 23 | // during development we search up from the build directory to find the Assets folder 24 | // (instead of copying all the Assets to the build directory). 25 | if (path == null) 26 | { 27 | var up = ""; 28 | while (!Directory.Exists(Path.Join(up, assetsFolderName)) && up.Length < 10) 29 | up = Path.Join(up, ".."); 30 | path = Path.Join(up, assetsFolderName); 31 | } 32 | 33 | return path ?? throw new Exception("Unable to find Assets path"); 34 | } 35 | } 36 | 37 | public static void Load(GraphicsDevice gfx) 38 | { 39 | var spritesPath = Path.Join(AssetsPath, "Sprites"); 40 | var tilesetsPath = Path.Join(AssetsPath, "Tilesets"); 41 | var spriteFiles = new Dictionary(); 42 | var tilesetFiles = new Dictionary(); 43 | 44 | // load main font file 45 | Font = new SpriteFont(gfx, Path.Join(AssetsPath, "Fonts", "dogica.ttf"), 8); 46 | Font.LineGap = 4; 47 | 48 | // get sprite files 49 | foreach (var file in Directory.EnumerateFiles(spritesPath, "*.ase", SearchOption.AllDirectories)) 50 | { 51 | var name = Path.ChangeExtension(Path.GetRelativePath(spritesPath, file), null); 52 | var ase = new Aseprite(file); 53 | if (ase.Frames.Length > 0) 54 | spriteFiles.Add(name, ase); 55 | } 56 | 57 | // get tileset files 58 | foreach (var file in Directory.EnumerateFiles(tilesetsPath, "*.ase", SearchOption.AllDirectories)) 59 | { 60 | var name = Path.ChangeExtension(Path.GetRelativePath(tilesetsPath, file), null); 61 | var ase = new Aseprite(file); 62 | if (ase.Frames.Length > 0) 63 | tilesetFiles.Add(name, ase); 64 | } 65 | 66 | // pack all the sprites & tilesets 67 | Packer.Output output; 68 | { 69 | var packer = new Packer(); 70 | 71 | foreach (var (name, ase) in spriteFiles) 72 | { 73 | var frames = ase.RenderAllFrames(); 74 | for (int i = 0; i < frames.Length; i ++) 75 | packer.Add($"{name}/{i}", frames[i]); 76 | } 77 | 78 | foreach (var (name, ase) in tilesetFiles) 79 | { 80 | var image = ase.RenderFrame(0); 81 | var columns = image.Width / Game.TileSize; 82 | var rows = image.Height / Game.TileSize; 83 | 84 | for (int x = 0; x < columns; x ++) 85 | for (int y = 0; y < rows; y ++) 86 | packer.Add($"tilesets/{name}{x}x{y}", image, new RectInt(x, y, 1, 1) * Game.TileSize); 87 | } 88 | 89 | output = packer.Pack(); 90 | } 91 | 92 | // create texture file 93 | Atlas = new Texture(gfx, output.Pages[0], name: "Atlas"); 94 | 95 | // create subtextures 96 | foreach (var it in output.Entries) 97 | Subtextures.Add(it.Name, new Subtexture(Atlas, it.Source, it.Frame)); 98 | 99 | // create sprite assets 100 | foreach (var (name, ase) in spriteFiles) 101 | { 102 | // find origin 103 | Vector2 origin = Vector2.Zero; 104 | if (ase.Slices.Count > 0 && ase.Slices[0].Keys.Length > 0 && ase.Slices[0].Keys[0].Pivot.HasValue) 105 | origin = ase.Slices[0].Keys[0].Pivot!.Value; 106 | 107 | var sprite = new Sprite(name, origin); 108 | 109 | // add frames 110 | for (int i = 0; i < ase.Frames.Length; i ++) 111 | sprite.Frames.Add(new(GetSubtexture($"{name}/{i}"), ase.Frames[i].Duration / 1000.0f)); 112 | 113 | // add animations 114 | foreach (var tag in ase.Tags) 115 | { 116 | if (!string.IsNullOrEmpty(tag.Name)) 117 | sprite.AddAnimation(tag.Name, tag.From, tag.To - tag.From + 1); 118 | } 119 | 120 | Sprites.Add(name, sprite); 121 | } 122 | 123 | // create tileset assets 124 | foreach (var (name, ase) in tilesetFiles) 125 | { 126 | var columns = ase.Width / Game.TileSize; 127 | var rows = ase.Height / Game.TileSize; 128 | var tileset = new Tileset(name, columns, rows); 129 | 130 | for (int x = 0; x < columns; x ++) 131 | for (int y = 0; y < rows; y ++) 132 | tileset.Tiles[x + y * columns] = GetSubtexture($"tilesets/{name}{x}x{y}"); 133 | 134 | Tilesets.Add(name, tileset); 135 | } 136 | 137 | // load rooms 138 | foreach (var file in Directory.EnumerateFiles(Path.Join(AssetsPath, "Rooms"), "*.txt")) 139 | { 140 | var name = Path.GetFileNameWithoutExtension(file).Split('x'); 141 | if (name.Length <= 1) 142 | continue; 143 | 144 | if (!int.TryParse(name[0], out var x) || !int.TryParse(name[1], out var y)) 145 | continue; 146 | 147 | var p = new Point2(x, y); 148 | Rooms.Add(p, new Room(p, file)); 149 | } 150 | } 151 | 152 | public static void Unload() 153 | { 154 | Atlas?.Dispose(); 155 | Atlas = null; 156 | Font = null; 157 | 158 | Sprites.Clear(); 159 | Tilesets.Clear(); 160 | Subtextures.Clear(); 161 | Rooms.Clear(); 162 | } 163 | 164 | public static Sprite? GetSprite(string name) 165 | { 166 | if (Sprites.TryGetValue(name, out var value)) 167 | return value; 168 | return null; 169 | } 170 | 171 | public static Tileset? GetTileset(string name) 172 | { 173 | if (Tilesets.TryGetValue(name, out var value)) 174 | return value; 175 | return null; 176 | } 177 | 178 | public static Room? GetRoom(Point2 cell) 179 | { 180 | if (Rooms.TryGetValue(cell, out var value)) 181 | return value; 182 | return null; 183 | } 184 | 185 | public static Subtexture GetSubtexture(string name) 186 | { 187 | if (Subtextures.TryGetValue(name, out var value)) 188 | return value; 189 | return new(); 190 | } 191 | 192 | } -------------------------------------------------------------------------------- /TinyLink/Source/Assets/Room.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Text; 3 | using Foster.Framework; 4 | 5 | namespace TinyLink; 6 | 7 | public class Room 8 | { 9 | /// 10 | /// World Location of the Room 11 | /// 12 | public readonly Point2 Cell; 13 | 14 | /// 15 | /// Grid of Tiles 16 | /// 17 | public readonly char[,] Tiles = new char[Game.Columns, Game.Rows]; 18 | 19 | /// 20 | /// Optional Text string. There's no way to edit this in-game, but 21 | /// it is used to display title/ending text. 22 | /// 23 | public string Text = string.Empty; 24 | 25 | /// 26 | /// Bounds in World Space in Pixels 27 | /// 28 | public RectInt WorldBounds => new(Cell.X * Game.Width, Cell.Y * Game.Height, Game.Width, Game.Height); 29 | 30 | public Room(Point2 cell) 31 | { 32 | Cell = cell; 33 | Text = string.Empty; 34 | for (int x = 0; x < Game.Columns; x ++) 35 | for (int y = 0; y < Game.Rows; y ++) 36 | Tiles[x, y] = '0'; 37 | } 38 | 39 | public Room(Point2 cell, string path) 40 | { 41 | Cell = cell; 42 | 43 | var lines = File.ReadAllLines(path); 44 | var text = new StringBuilder(); 45 | var y = 0; 46 | 47 | foreach (var line in lines) 48 | { 49 | // parse text line 50 | if (line.StartsWith(":")) 51 | { 52 | text.AppendLine(line[1..]); 53 | } 54 | // parse row of tiles 55 | else if (y < Game.Rows) 56 | { 57 | for (int x = 0; x < line.Length && x < Game.Columns; x ++) 58 | Tiles[x, y] = line[x]; 59 | y++; 60 | } 61 | } 62 | 63 | Text = text.ToString(); 64 | } 65 | 66 | public void Set(Point2 tile, char ch) 67 | { 68 | if (tile.X >= 0 && tile.Y >= 0 && tile.X < Game.Columns && tile.Y < Game.Rows) 69 | Tiles[tile.X, tile.Y] = ch; 70 | } 71 | 72 | public void Save() 73 | { 74 | StringBuilder result = new(); 75 | 76 | // write text lines first 77 | foreach (var line in Text.Split(new string[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries)) 78 | result.AppendLine($":{line}"); 79 | 80 | // write tile grid next 81 | for (int y = 0; y < Game.Rows; y ++) 82 | { 83 | for (int x = 0; x < Game.Columns; x ++) 84 | result.Append(Tiles[x, y]); 85 | result.AppendLine(); 86 | } 87 | 88 | // output to file 89 | File.WriteAllText( 90 | Path.Join(Assets.AssetsPath, "Rooms", $"{Cell.X}x{Cell.Y}.txt"), 91 | result.ToString()); 92 | } 93 | } -------------------------------------------------------------------------------- /TinyLink/Source/Assets/Sprite.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Numerics; 3 | using Foster.Framework; 4 | 5 | namespace TinyLink; 6 | 7 | public class Sprite 8 | { 9 | public readonly record struct Frame(Subtexture Subtexture, float Duration); 10 | public readonly record struct Animation(string Name, int FrameStart, int FrameCount, float Duration); 11 | 12 | public readonly string Name; 13 | public readonly Vector2 Origin; 14 | public readonly List Frames = new(); 15 | public readonly List Animations = new(); 16 | 17 | public Sprite(string name, Vector2 origin) 18 | { 19 | Name = name; 20 | Origin = origin; 21 | } 22 | 23 | public Frame GetFrameAt(in Animation animation, float time, bool loop) 24 | { 25 | if (time >= animation.Duration && !loop) 26 | return Frames[animation.FrameStart + animation.FrameCount - 1]; 27 | 28 | time %= animation.Duration; 29 | for (int i = animation.FrameStart; i < animation.FrameStart + animation.FrameCount; i ++) 30 | { 31 | time -= Frames[i].Duration; 32 | if (time <= 0) 33 | return Frames[i]; 34 | } 35 | return Frames[animation.FrameStart]; 36 | } 37 | 38 | public void AddAnimation(string name, int frameStart, int frameCount) 39 | { 40 | float duration = 0; 41 | for (int i = frameStart; i < frameStart + frameCount; i ++) 42 | duration += Frames[i].Duration; 43 | Animations.Add(new(name, frameStart, frameCount, duration)); 44 | } 45 | 46 | public Animation? GetAnimation(string? name) 47 | { 48 | if (name != null) 49 | { 50 | foreach (var it in Animations) 51 | if (it.Name == name) 52 | return it; 53 | } 54 | 55 | return null; 56 | } 57 | } -------------------------------------------------------------------------------- /TinyLink/Source/Assets/Tileset.cs: -------------------------------------------------------------------------------- 1 | 2 | using Foster.Framework; 3 | 4 | namespace TinyLink; 5 | 6 | public class Tileset 7 | { 8 | public readonly string Name; 9 | public readonly int Columns; 10 | public readonly int Rows; 11 | public readonly List Tiles = new(); 12 | 13 | public Tileset(string name, int columns, int rows) 14 | { 15 | Name = name; 16 | Columns = columns; 17 | Rows = rows; 18 | for (int i = 0; i < columns * rows; i ++) 19 | Tiles.Add(new()); 20 | } 21 | 22 | public Subtexture GetRandomTile(ref Rng rng) 23 | => Tiles[rng.Int(Tiles.Count)]; 24 | } -------------------------------------------------------------------------------- /TinyLink/Source/Controls.cs: -------------------------------------------------------------------------------- 1 | 2 | using Foster.Framework; 3 | 4 | namespace TinyLink; 5 | 6 | public class Controls(Input input) 7 | { 8 | public readonly VirtualStick Move = new(input, "Move", 9 | new StickBindingSet() 10 | .AddArrowKeys() 11 | .AddDPad() 12 | .Add(Axes.LeftX, 0.25f, Axes.LeftY, 0.50f, 0.25f) 13 | ); 14 | 15 | public readonly VirtualAction Jump = new(input, "Jump", 16 | new ActionBindingSet() 17 | .Add(Keys.X) 18 | .Add(Buttons.South) 19 | ); 20 | 21 | public readonly VirtualAction Attack = new(input, "Attack", 22 | new ActionBindingSet() 23 | .Add(Keys.C) 24 | .Add(Buttons.West) 25 | ); 26 | } -------------------------------------------------------------------------------- /TinyLink/Source/Factory.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | using Foster.Framework; 3 | 4 | namespace TinyLink; 5 | 6 | /// 7 | /// The Factory holds meta information on how to populate the Game when loading a Room. 8 | /// It describes Actor Spawns and Tile types in a single struct called "Entry". 9 | /// 10 | public static class Factory 11 | { 12 | /// 13 | /// Used to Spawn an Actor 14 | /// 15 | public delegate void SpawnFn(Point2 position, Game game); 16 | 17 | /// 18 | /// Used to Place a Tile 19 | /// 20 | public delegate void TileFn(Point2 tile, Grid fg, Grid bg); 21 | 22 | public readonly struct Entry 23 | { 24 | public readonly string Name; 25 | public readonly Subtexture Image; 26 | public readonly Vector2 Origin; 27 | public readonly Point2 Offset; 28 | public readonly SpawnFn? Spawn; 29 | public readonly TileFn? Tile; 30 | 31 | public Entry(string name, Subtexture image, Vector2 origin, Point2 offset, SpawnFn spawn) 32 | { 33 | Name = name; 34 | Image = image; 35 | Origin = origin; 36 | Offset = offset; 37 | Spawn = spawn; 38 | } 39 | 40 | public Entry(string name, Subtexture image, TileFn tile) 41 | { 42 | Name = name; 43 | Image = image; 44 | Tile = tile; 45 | } 46 | 47 | public static Entry AsActor(string spriteName, Point2? offset = null, bool exclusive = false, Action? additional = null) where T : Actor, new() 48 | { 49 | var sprite = Assets.GetSprite(spriteName); 50 | if (sprite == null) 51 | return new(); 52 | 53 | return new Entry(typeof(T).Name, sprite.Frames[0].Subtexture, sprite.Origin, offset ?? Point2.Zero, (position, game) => 54 | { 55 | if (!exclusive || game.GetFirst() == null) 56 | { 57 | var it = game.Create(position); 58 | additional?.Invoke(it); 59 | } 60 | }); 61 | } 62 | 63 | public static Entry AsTile(string tilesetName, bool isFg) 64 | { 65 | var tileset = Assets.GetTileset(tilesetName); 66 | if (tileset == null) 67 | return new(); 68 | 69 | return new Entry(tileset.Name, tileset.Tiles[0], (Point2 tile, Grid fg, Grid bg) => 70 | { 71 | Rng rng = new(tile.X + tile.Y * Game.Columns); 72 | (isFg ? fg : bg).Set(tile.X, tile.Y, tileset.GetRandomTile(ref rng)); 73 | }); 74 | } 75 | } 76 | 77 | /// 78 | /// List of all the Types 79 | /// 80 | public static readonly Dictionary Entries = new(); 81 | 82 | /// 83 | /// Registers a new Type 84 | /// 85 | public static void Register(char id, in Entry entry) 86 | { 87 | Entries[id] = entry; 88 | } 89 | 90 | /// 91 | /// Finds a given Type entry by its character ID 92 | /// 93 | public static Entry? Find(char id) 94 | { 95 | if (Entries.TryGetValue(id, out var entry)) 96 | return entry; 97 | 98 | return null; 99 | } 100 | 101 | /// 102 | /// Registers the Default types built into the game. 103 | /// 104 | public static void RegisterTypes() 105 | { 106 | // Empty Type, purely for the Editord 107 | Register('0', new Entry("Empty", new Subtexture(), null!)); 108 | 109 | // Add Tile Types 110 | Register('1', Entry.AsTile("castle", true)); 111 | Register('G', Entry.AsTile("grass", true)); 112 | Register('g', Entry.AsTile("plants", false)); 113 | Register('w', Entry.AsTile("water", false)); 114 | Register('#', Entry.AsTile("back", false)); 115 | 116 | // Add Actor Types 117 | Register('-', Entry.AsActor("jumpthru")); 118 | Register('P', Entry.AsActor("player", new Point2(4, 8), true)); 119 | Register('B', Entry.AsActor("bramble", new Point2(4, 8))); 120 | Register('S', Entry.AsActor("spitter", new Point2(4, 8))); 121 | Register('M', Entry.AsActor("mosquito", new Point2(4, 4))); 122 | Register('D', Entry.AsActor("door", new Point2(4, 8), false, (it) => it.Appear())); 123 | Register('C', Entry.AsActor("door", new Point2(4, 8))); 124 | Register('b', Entry.AsActor("blob", new Point2(4, 8))); 125 | Register('F', Entry.AsActor("ghostfrog", new Point2(4, 8))); 126 | Register('T', Entry.AsActor("heart", new Point2(4, 4))); 127 | } 128 | 129 | public static void Clear() 130 | { 131 | Entries.Clear(); 132 | } 133 | 134 | } -------------------------------------------------------------------------------- /TinyLink/Source/Game.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | using Foster.Framework; 3 | 4 | namespace TinyLink; 5 | 6 | public class Game 7 | { 8 | public const int Width = 240; 9 | public const int Height = 135; 10 | public const int Columns = Width / TileSize; 11 | public const int Rows = Height / TileSize + 1; 12 | public const int TileSize = 8; 13 | 14 | public readonly Manager Manager; 15 | public readonly Controls Controls; 16 | public readonly Batcher Batcher; 17 | public readonly Target Screen; 18 | public readonly List Actors = []; 19 | 20 | public Input Input => Manager.Input; 21 | public GraphicsDevice GraphicsDevice => Manager.GraphicsDevice; 22 | public Time Time => Manager.Time; 23 | 24 | private Room? room; 25 | private Room? nextRoom; 26 | private float nextRoomEase; 27 | private readonly List destroying = []; 28 | private readonly List rendering = []; 29 | private float hitstun = 0; 30 | private float shaking = 0; 31 | private Point2 shake; 32 | private Rng rng = new(); 33 | 34 | public Vector2 Camera { get; private set; } 35 | public RectInt Bounds => room?.WorldBounds ?? new(); 36 | public Point2 Cell => room?.Cell ?? Point2.Zero; 37 | public Room? CurrentRoom => room; 38 | 39 | public Game(Manager manager, Point2 start) 40 | { 41 | Manager = manager; 42 | Batcher = new(manager.GraphicsDevice, name: "GameBatcher"); 43 | Screen = new(manager.GraphicsDevice, Width, Height, name: "GameScreen"); 44 | Controls = new(manager.Input); 45 | 46 | if (Assets.Rooms.TryGetValue(start, out room)) 47 | { 48 | Camera = new Vector2(room.Cell.X * Width, room.Cell.Y * Height); 49 | LoadRoom(room); 50 | } 51 | } 52 | 53 | public void Update() 54 | { 55 | // Don't run normal gameplay loop if no room is loaded 56 | if (room == null) 57 | return; 58 | 59 | // Run Game normally when not moving to a new room 60 | if (nextRoom == null) 61 | { 62 | // Reload Room Debug 63 | if (Input.Keyboard.Pressed(Keys.R)) 64 | ReloadRoom(); 65 | 66 | // only run normal updates if no hitstun 67 | if (hitstun <= 0) 68 | { 69 | // Remove Destroyed Actors 70 | for (int i = 0; i < destroying.Count; i ++) 71 | { 72 | destroying[i].Destroyed(); 73 | Actors.Remove(destroying[i]); 74 | } 75 | destroying.Clear(); 76 | 77 | // Update Actors 78 | for (int i = 0; i < Actors.Count; i ++) 79 | Actors[i].Update(); 80 | 81 | // screen shaking 82 | if (shaking > 0) 83 | { 84 | shaking -= Time.Delta; 85 | if (Time.OnInterval(0.05f)) 86 | shake = new(rng.Sign(), rng.Sign()); 87 | } 88 | else 89 | shake = Point2.Zero; 90 | } 91 | else 92 | hitstun -= Time.Delta; 93 | } 94 | // Lerp the Camera to the new Room 95 | else if (nextRoomEase < 1.0f) 96 | { 97 | Camera = Vector2.Lerp(room.WorldBounds.TopLeft, nextRoom.WorldBounds.TopLeft, Ease.Cube.InOut(nextRoomEase)); 98 | nextRoomEase = Calc.Approach(nextRoomEase, 1.0f, Time.Delta * 4.0f); 99 | } 100 | // Finished Lerping the Camera to the new room, return to normal update 101 | else 102 | { 103 | room = nextRoom; 104 | nextRoom = null; 105 | Hitstun(0.1f); 106 | } 107 | } 108 | 109 | public void Render(in RectInt viewport) 110 | { 111 | // draw gameplay to screen 112 | Screen.Clear(0x150e22); 113 | Batcher.PushMatrix(-(Point2)Camera + shake); 114 | 115 | // draw actors 116 | rendering.AddRange(Actors); 117 | rendering.Sort((a, b) => b.Depth - a.Depth); 118 | foreach (var actor in rendering) 119 | { 120 | if (!actor.Visible) 121 | continue; 122 | 123 | Batcher.PushMatrix(actor.Position); 124 | actor.Render(Batcher); 125 | Batcher.PopMatrix(); 126 | } 127 | rendering.Clear(); 128 | Batcher.PopMatrix(); 129 | 130 | // draw player HP 131 | if (GetFirst() is Player player) 132 | { 133 | Point2 pos = new(0, Height - 16); 134 | Batcher.Rect(new Rect(pos.X, pos.Y + 7, 48, 4), Color.Black); 135 | 136 | for (int i = 0; i < Player.MaxHealth; i++) 137 | { 138 | if (player.Health >= i + 1) 139 | Batcher.Image(Assets.GetSubtexture("heart/0"), pos, Color.White); 140 | else 141 | Batcher.Image(Assets.GetSubtexture("heart/1"), pos, Color.White); 142 | pos.X += 12; 143 | } 144 | } 145 | 146 | Batcher.Render(Screen); 147 | Batcher.Clear(); 148 | 149 | // draw screen to window 150 | { 151 | var size = viewport.Size; 152 | var center = viewport.Center; 153 | var scale = Calc.Min(size.X / (float)Screen.Width, size.Y / (float)Screen.Height); 154 | 155 | Batcher.PushSampler(new(TextureFilter.Nearest, TextureWrap.Clamp, TextureWrap.Clamp)); 156 | Batcher.Image(Screen, center, Screen.Bounds.Size / 2, Vector2.One * scale, 0, Color.White); 157 | Batcher.PopSampler(); 158 | Batcher.Render(Manager.Window); 159 | Batcher.Clear(); 160 | } 161 | } 162 | 163 | public T Create(Point2? position = null) where T : Actor, new() 164 | { 165 | var instance = new T 166 | { 167 | Game = this, 168 | Position = position ?? Point2.Zero 169 | }; 170 | instance.Added(); 171 | Actors.Add(instance); 172 | return instance; 173 | } 174 | 175 | public T? GetFirst() where T : Actor 176 | { 177 | foreach (var it in Actors) 178 | if (it is T instance && !destroying.Contains(it)) 179 | return instance; 180 | return null; 181 | } 182 | 183 | public Actor? GetFirst(Actor.Masks mask) 184 | { 185 | foreach (var it in Actors) 186 | if (it.Mask.Has(mask) && !destroying.Contains(it)) 187 | return it; 188 | return null; 189 | } 190 | 191 | public void Destroy(Actor actor) 192 | { 193 | if (!destroying.Contains(actor)) 194 | destroying.Add(actor); 195 | } 196 | 197 | public bool Transition(Point2 direction) 198 | { 199 | if (room != null && nextRoom == null) 200 | { 201 | var nextCell = room.Cell + direction; 202 | if (Assets.Rooms.TryGetValue(nextCell, out nextRoom)) 203 | { 204 | foreach (var it in Actors) 205 | if (it is not Player) 206 | Destroy(it); 207 | 208 | nextRoomEase = 0.0f; 209 | LoadRoom(nextRoom); 210 | return true; 211 | } 212 | } 213 | 214 | return false; 215 | } 216 | 217 | public bool OverlapsAll(in RectInt rect, Actor.Masks mask, List results) 218 | { 219 | foreach (var actor in Actors) 220 | { 221 | var local = rect - actor.Position; 222 | if (actor.Mask.Has(mask) && actor.Hitbox.Overlaps(local)) 223 | results.Add(actor); 224 | } 225 | 226 | return results.Count > 0; 227 | } 228 | 229 | public Actor? OverlapsFirst(in RectInt rect, Actor.Masks mask) 230 | { 231 | foreach (var actor in Actors) 232 | { 233 | var local = rect - actor.Position; 234 | if (actor.Mask.Has(mask) && actor.Hitbox.Overlaps(local)) 235 | return actor; 236 | } 237 | 238 | return null; 239 | } 240 | 241 | public void ReloadRoom() 242 | { 243 | if (room != null) 244 | { 245 | foreach (var actor in Actors) 246 | Destroy(actor); 247 | LoadRoom(room); 248 | } 249 | } 250 | 251 | public void LoadRoom(Room room) 252 | { 253 | var offset = room.WorldBounds.TopLeft; 254 | 255 | var bg = Create(offset); 256 | bg.Depth = 10; 257 | 258 | var fg = Create(offset); 259 | fg.Mask = Actor.Masks.Solid; 260 | fg.Depth = 5; 261 | 262 | // loop over room grid placing objects 263 | for (int x = 0; x < Columns; x ++) 264 | for (int y = 0; y < Rows; y ++) 265 | { 266 | var tile = new Point2(x, y); 267 | var at = offset + tile * TileSize; 268 | 269 | if (Factory.Find(room.Tiles[x, y]) is Factory.Entry entry) 270 | { 271 | entry.Spawn?.Invoke(at + entry.Offset, this); 272 | entry.Tile?.Invoke(tile, fg, bg); 273 | } 274 | } 275 | } 276 | 277 | public void Hitstun(float time) => hitstun = MathF.Max(hitstun, time); 278 | 279 | public void Shake(float time) => shaking = MathF.Max(shaking, time); 280 | } -------------------------------------------------------------------------------- /TinyLink/Source/Hitbox.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | using Foster.Framework; 3 | 4 | namespace TinyLink; 5 | 6 | /// 7 | /// Hitbox can test overlaps between Rectangles and Grids 8 | /// 9 | public readonly struct Hitbox 10 | { 11 | public enum Shapes 12 | { 13 | Rect, 14 | Grid 15 | } 16 | 17 | public readonly Shapes Shape; 18 | private readonly RectInt rect; 19 | private readonly bool[,]? grid; 20 | 21 | public Hitbox() 22 | { 23 | Shape = Shapes.Rect; 24 | rect = new RectInt(0, 0, 0, 0); 25 | grid = null; 26 | } 27 | 28 | public Hitbox(in RectInt value) 29 | { 30 | Shape = Shapes.Rect; 31 | rect = value; 32 | grid = null; 33 | } 34 | 35 | public Hitbox(in bool[,] value) 36 | { 37 | Shape = Shapes.Grid; 38 | grid = value; 39 | } 40 | 41 | public bool Overlaps(in RectInt rect) 42 | => Overlaps(Point2.Zero, new Hitbox(rect)); 43 | 44 | public bool Overlaps(in Hitbox other) 45 | => Overlaps(Point2.Zero, other); 46 | 47 | public bool Overlaps(in Point2 offset, in Hitbox other) 48 | { 49 | switch (Shape) 50 | { 51 | case Shapes.Rect: 52 | switch (other.Shape) 53 | { 54 | case Shapes.Rect: return RectToRect(rect + offset, other.rect); 55 | case Shapes.Grid: return RectToGrid(rect + offset, other.grid!); 56 | } 57 | break; 58 | case Shapes.Grid: 59 | switch (other.Shape) 60 | { 61 | case Shapes.Rect: return RectToGrid(other.rect - offset, grid!); 62 | case Shapes.Grid: throw new NotImplementedException("Grid->Grid overlap not implemented!"); 63 | } 64 | break; 65 | } 66 | 67 | throw new NotImplementedException(); 68 | } 69 | 70 | private static bool RectToRect(in RectInt a, in RectInt b) 71 | { 72 | return a.Overlaps(b); 73 | } 74 | 75 | private static bool RectToGrid(in RectInt a, in bool[,] grid) 76 | { 77 | int left = Calc.Clamp((int)Math.Floor(a.Left / (float)Game.TileSize), 0, grid.GetLength(0)); 78 | int right = Calc.Clamp((int)Math.Ceiling(a.Right / (float)Game.TileSize), 0, grid.GetLength(0)); 79 | int top = Calc.Clamp((int)Math.Floor(a.Top / (float)Game.TileSize), 0, grid.GetLength(1)); 80 | int bottom = Calc.Clamp((int)Math.Ceiling(a.Bottom / (float)Game.TileSize), 0, grid.GetLength(1)); 81 | 82 | for (int x = left; x < right; x ++) 83 | for (int y = top; y < bottom; y ++) 84 | if (grid[x, y]) 85 | return true; 86 | 87 | return false; 88 | } 89 | 90 | public void Render(Batcher batcher, Point2 offset, Color color) 91 | { 92 | batcher.PushMatrix(offset); 93 | 94 | if (Shape == Shapes.Rect) 95 | { 96 | batcher.RectLine(rect + offset, 1, color); 97 | } 98 | else if (Shape == Shapes.Grid && grid != null) 99 | { 100 | for (int x = 0; x < grid.GetLength(0); x ++) 101 | for (int y = 0; y < grid.GetLength(1); y ++) 102 | { 103 | if (grid[x, y]) 104 | batcher.RectLine(new RectInt(x, y, 1, 1) * Game.TileSize, 1, color); 105 | } 106 | } 107 | 108 | batcher.PopMatrix(); 109 | } 110 | 111 | } -------------------------------------------------------------------------------- /TinyLink/Source/Manager.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Numerics; 3 | using Foster.Framework; 4 | 5 | namespace TinyLink; 6 | 7 | class Program 8 | { 9 | public static void Main() 10 | { 11 | using var manager = new Manager(); 12 | manager.Run(); 13 | } 14 | } 15 | 16 | /// 17 | /// Runs the Game and, when the game is not running, displays a tiny tile-based Level Editor 18 | /// 19 | public class Manager : App 20 | { 21 | private const float Scale = 3.0f; 22 | private const float ButtonSize = 24; 23 | private const float ButtonSpacing = 2; 24 | private const float BoxPadding = 4; 25 | private const float BoxSpacing = 12; 26 | private const int PaletteColumns = 4; 27 | private const int ToolbarSpacing = 4; 28 | private const float ScreenPadding = 4; 29 | private const float ToolbarHeight = ButtonSize + 4; 30 | private const float EditorInset = 24; 31 | private const float PaletteWidth = BoxPadding * 2 + PaletteColumns * ButtonSize + (PaletteColumns - 1) * ButtonSpacing; 32 | private readonly Color highlight = 0x5fcde4; 33 | 34 | /// 35 | /// The current Room Cell we're viewing 36 | /// 37 | private Point2 cell = new(0, 0); 38 | 39 | /// 40 | /// Our game, if it's running 41 | /// 42 | private Game? game; 43 | 44 | /// 45 | /// Eases in/out as the Game starts/stops plaing 46 | /// 47 | private float gameEase = 1.0f; 48 | 49 | /// 50 | /// Sprite Batcher for general Editor visuals 51 | /// 52 | private readonly Batcher batcher; 53 | 54 | /// 55 | /// Visible Space 56 | /// 57 | private Rect View 58 | => new Rect(0, 0, Window.WidthInPixels / Scale, Window.HeightInPixels / Scale).Inflate(-ScreenPadding); 59 | 60 | /// 61 | /// Toolbar Space 62 | /// 63 | private Rect ToolbarRect 64 | => new (View.X, View.Y, View.Width, ToolbarHeight); 65 | 66 | /// 67 | /// Workspace, which is the full space below the Toolbar 68 | /// 69 | private Rect WorkRect 70 | => new (View.X, View.Y + ToolbarSpacing + ToolbarHeight, View.Width, View.Height - ToolbarHeight - ToolbarSpacing); 71 | 72 | /// 73 | /// Palette View when the Editor is Open 74 | /// 75 | private Rect PaletteOpenRect 76 | => new (WorkRect.X, WorkRect.Y, PaletteWidth, WorkRect.Height); 77 | 78 | /// 79 | /// Edit Workspace when the Editor is Open 80 | /// 81 | private Rect EditorOpenRect 82 | => new Rect(WorkRect.X + PaletteWidth + BoxSpacing, WorkRect.Y, WorkRect.Width - PaletteWidth - BoxSpacing, WorkRect.Height).Inflate(-EditorInset); 83 | 84 | /// 85 | /// Play Workspace when the Game is Open 86 | /// 87 | private Rect GameOpenRect => WorkRect; 88 | 89 | /// 90 | /// Eases between the EditView and the PlayView as the Game opens/closes 91 | /// 92 | private Rect WorkspaceRect 93 | { 94 | get 95 | { 96 | var a = Vector2.Lerp(EditorOpenRect.TopLeft, GameOpenRect.TopLeft, Ease.Cube.InOut(gameEase)); 97 | var b = Vector2.Lerp(EditorOpenRect.BottomRight, GameOpenRect.BottomRight, Ease.Cube.InOut(gameEase)); 98 | return Rect.Between(a, b); 99 | } 100 | } 101 | 102 | /// 103 | /// Eases the PaletteView out/in as the Game opens/closes 104 | /// 105 | /// 106 | private Rect PaletteSlideRect => 107 | PaletteOpenRect - Vector2.UnitX * Ease.Cube.InOut(gameEase) * PaletteOpenRect.Right; 108 | 109 | /// 110 | /// Mouse Cursor relative to Scale 111 | /// 112 | private Vector2 Cursor => Input.Mouse.Position / Scale; 113 | 114 | /// 115 | /// Line Weight for outlines, relative to Scale 116 | /// 117 | private float LineWeight => (1.0f / Scale) * 4; 118 | 119 | /// 120 | /// Tries to get the Existing Room 121 | /// 122 | private Room? CurrentRoom => Assets.GetRoom(cell); 123 | 124 | /// 125 | /// The current Type we're painting in the Editor 126 | /// 127 | private char brush = '0'; 128 | 129 | /// 130 | /// If it's a big Brush 131 | /// 132 | private bool brushBig = false; 133 | 134 | private Point2 tilePlaceFrom; 135 | private char tileTypePlacing; 136 | private bool tilePlacing; 137 | 138 | public Manager() : base(new AppConfig() 139 | { 140 | ApplicationName = "TinyLink", 141 | WindowTitle = "TinyLink", 142 | Width = 1280, 143 | Height = 720, 144 | Resizable = true 145 | }) 146 | { 147 | batcher = new(GraphicsDevice); 148 | } 149 | 150 | protected override void Startup() 151 | { 152 | Assets.Load(GraphicsDevice); 153 | Factory.RegisterTypes(); 154 | 155 | game = new Game(this, cell); 156 | } 157 | 158 | protected override void Shutdown() 159 | { 160 | Assets.Unload(); 161 | Factory.Clear(); 162 | } 163 | 164 | protected override void Update() 165 | { 166 | // Misc. Hotkeys 167 | if (Input.Keyboard.Pressed(Keys.F1) && game == null) 168 | Reload(); 169 | if (Input.Keyboard.Pressed(Keys.F4)) 170 | Window.Fullscreen = !Window.Fullscreen; 171 | if (Input.Keyboard.Pressed(Keys.Escape)) 172 | { 173 | CurrentRoom?.Save(); 174 | Exit(); 175 | } 176 | 177 | // Run Game 178 | if (game != null) 179 | { 180 | gameEase = Calc.Approach(gameEase, 1.0f, Time.Delta * 4.0f); 181 | game.Update(); 182 | } 183 | // Show Editor 184 | else 185 | { 186 | gameEase = Calc.Approach(gameEase, 0.0f, Time.Delta * 4.0f); 187 | } 188 | 189 | // Build our Sprite Batch 190 | { 191 | batcher.Clear(); 192 | batcher.PushMatrix(Matrix3x2.CreateScale(Scale)); 193 | 194 | // draw editor/game box 195 | Box(WorkspaceRect); 196 | 197 | // draw editor stuff if it's open 198 | if (gameEase < 1.0f) 199 | { 200 | Palette(); 201 | Editor(); 202 | } 203 | 204 | Toolbar(); 205 | 206 | // draw to screen 207 | batcher.PopMatrix(); 208 | } 209 | } 210 | 211 | protected override void Render() 212 | { 213 | // draw the main UI first 214 | Window.Clear(0x2e1426); 215 | batcher.Render(Window); 216 | 217 | // draw game on top if it exists 218 | game?.Render((RectInt)(WorkspaceRect.Inflate(-BoxPadding) * Scale)); 219 | } 220 | 221 | private void Reload() 222 | { 223 | Assets.Unload(); 224 | Factory.Clear(); 225 | 226 | Assets.Load(GraphicsDevice); 227 | Factory.RegisterTypes(); 228 | } 229 | 230 | /// 231 | /// Shifts to display a new room 232 | /// 233 | private void ShiftRoom(Point2 direction) 234 | { 235 | // save last room 236 | CurrentRoom?.Save(); 237 | 238 | // set next room 239 | cell += direction; 240 | } 241 | 242 | /// 243 | /// Draws a box outline (for palette / workspace) 244 | /// 245 | private void Box(Rect box) 246 | { 247 | batcher.Rect(box, 0x000000); 248 | batcher.RectLine(box, LineWeight, 0xffffff); 249 | } 250 | 251 | /// 252 | /// Draws a Pressable button with an icon and tooltip 253 | /// 254 | private bool Button(Rect button, Subtexture subtexture, string text, bool selected = false) 255 | { 256 | var hovering = button.Contains(Cursor); 257 | 258 | batcher.Rect(button, Color.Black); 259 | batcher.ImageFit(subtexture, button.Inflate(-LineWeight), Vector2.One * 0.50f, Color.White, false, false); 260 | batcher.RectRoundedLine(button, 4, LineWeight, hovering ? Color.White : (selected ? highlight : Color.Gray)); 261 | 262 | if (hovering && Assets.Font != null) 263 | { 264 | var at = new Rect( 265 | Cursor.X + 32, 266 | Cursor.Y - Assets.Font.LineHeight / 2, 267 | Assets.Font.WidthOf(text), 268 | Assets.Font.LineHeight).Inflate(12, 6, 12, 6); 269 | 270 | at.X = Calc.Clamp(at.X, View.Left + 8, View.Right - at.Width - 8); 271 | at.Y = Calc.Clamp(at.Y, View.Top + 8, View.Bottom - at.Height - 8); 272 | 273 | if (at.Contains(Cursor)) 274 | at.X = Cursor.X - at.Width - 8; 275 | 276 | batcher.PushLayer(-10); 277 | batcher.RectRounded(at, 4, Color.DarkGray); 278 | batcher.RectRoundedLine(at, 4, LineWeight, Color.Gray); 279 | batcher.Text(Assets.Font, text, at.Center, Vector2.One * 0.50f, Color.White); 280 | batcher.PopLayer(); 281 | } 282 | 283 | return hovering && Input.Mouse.LeftPressed; 284 | } 285 | 286 | /// 287 | /// Displays the Toolbar 288 | /// 289 | private void Toolbar() 290 | { 291 | var rect = new Rect(ToolbarRect.X, ToolbarRect.Y, ButtonSize, ButtonSize); 292 | 293 | if (game == null) 294 | { 295 | if (Button(rect, Assets.GetSubtexture("buttons/0"), "Play [Space]") || 296 | Input.Keyboard.Pressed(Keys.Space)) 297 | { 298 | CurrentRoom?.Save(); 299 | if (CurrentRoom != null) 300 | game = new Game(this, cell); 301 | } 302 | rect.X += rect.Width + ButtonSpacing; 303 | 304 | if (Button(rect, Assets.GetSubtexture("buttons/7"), "Small Brush", !brushBig)) 305 | brushBig = false; 306 | rect.X += rect.Width + ButtonSpacing; 307 | if (Button(rect, Assets.GetSubtexture("buttons/8"), "Big Brush", brushBig)) 308 | brushBig = true; 309 | rect.X += rect.Width + ButtonSpacing; 310 | } 311 | else 312 | { 313 | if (Button(rect, Assets.GetSubtexture("buttons/1"), "Stop [Space]") || 314 | Input.Keyboard.Pressed(Keys.Space)) 315 | { 316 | cell = game.Cell; 317 | game = null; 318 | } 319 | } 320 | 321 | 322 | if (Assets.Font != null) 323 | { 324 | var cell = (game?.Cell ?? this.cell); 325 | batcher.Text(Assets.Font, $"Room {cell.X} x {cell.Y}", ToolbarRect.Center, new Vector2(0.50f, 0.50f), Color.White); 326 | } 327 | } 328 | 329 | /// 330 | /// Displays the Palette type buttons 331 | /// 332 | private void Palette() 333 | { 334 | Box(PaletteSlideRect); 335 | 336 | var bounds = PaletteSlideRect.Inflate(-BoxPadding); 337 | var at = bounds.TopLeft; 338 | var index = 0; 339 | 340 | foreach (var (id, it) in Factory.Entries) 341 | { 342 | var btn = new Rect(at.X, at.Y, ButtonSize, ButtonSize); 343 | if (Button(btn, it.Image, it.Name, brush == id)) 344 | brush = id; 345 | 346 | at.X += ButtonSize + ButtonSpacing; 347 | if (index > 0 && (index + 1) % PaletteColumns == 0) 348 | { 349 | at.X = bounds.Left; 350 | at.Y += ButtonSize + ButtonSpacing; 351 | } 352 | index++; 353 | } 354 | } 355 | 356 | /// 357 | /// Displays the main Editor workspace 358 | /// 359 | private void Editor() 360 | { 361 | var bounds = WorkspaceRect; 362 | var inner = bounds.Inflate(-BoxPadding * 2); 363 | var arrow = new Rect().Inflate(ButtonSize / 2); 364 | var lastCell = cell; 365 | 366 | // buttons to move between rooms 367 | if (game == null) 368 | { 369 | var upImg = Assets.GetSubtexture("buttons/6"); 370 | var leftImg = Assets.GetSubtexture("buttons/4"); 371 | var downImg = Assets.GetSubtexture("buttons/3"); 372 | var rightImg = Assets.GetSubtexture("buttons/5"); 373 | 374 | var upExits = Assets.GetRoom(cell + Point2.Up) != null; 375 | var leftExits = Assets.GetRoom(cell + Point2.Left) != null; 376 | var downExits = Assets.GetRoom(cell + Point2.Down) != null; 377 | var rightExits = Assets.GetRoom(cell + Point2.Right) != null; 378 | 379 | batcher.PushLayer(-1); 380 | if (Button(arrow + bounds.TopCenter, upImg, "Move Up Room [W]", upExits) || Input.Keyboard.Pressed(Keys.W)) 381 | ShiftRoom(Point2.Up); 382 | if (Button(arrow + bounds.CenterLeft, leftImg, "Move Left Room [A]", leftExits) || Input.Keyboard.Pressed(Keys.A)) 383 | ShiftRoom(Point2.Left); 384 | if (Button(arrow + bounds.BottomCenter, rightImg, "Move Down Room [S]", downExits) || Input.Keyboard.Pressed(Keys.S)) 385 | ShiftRoom(Point2.Down); 386 | if (Button(arrow + bounds.CenterRight, downImg, "Move Right Room [D]", rightExits) || Input.Keyboard.Pressed(Keys.D)) 387 | ShiftRoom(Point2.Right); 388 | batcher.PopLayer(); 389 | } 390 | 391 | if (CurrentRoom is Room editing) 392 | { 393 | var width = Game.Width; 394 | var height = Game.Height; 395 | var scale = Math.Min(inner.Width / width, inner.Height / height); 396 | var gridColor = Color.White * 0.10f; 397 | var gridWeight = 1.0f / scale; 398 | 399 | var matrix = 400 | Matrix3x2.CreateTranslation(-new Vector2(width, height) / 2) * 401 | Matrix3x2.CreateScale(scale) * 402 | Matrix3x2.CreateTranslation(inner.Center); 403 | 404 | var localMouse = Vector2.Zero; 405 | var tileOver = Point2.Zero; 406 | if (Matrix3x2.Invert(matrix, out var inverse)) 407 | { 408 | localMouse = Vector2.Transform(Cursor, inverse); 409 | tileOver = (Point2)(localMouse / Game.TileSize); 410 | } 411 | var overGame = new Rect(0, 0, Game.Width, Game.Height).Contains(localMouse); 412 | 413 | batcher.PushMatrix(matrix); 414 | batcher.Rect(new Rect(0, 0, Game.Width, Game.Height), Color.Black); 415 | batcher.PushScissor((RectInt)(inner * Scale)); 416 | 417 | // draw "tiles" first 418 | for (int x = 0; x < Game.Columns; x ++) 419 | for (int y = 0; y < Game.Rows; y ++) 420 | { 421 | if (Factory.Find(editing.Tiles[x, y]) is not {} it || it.Image.Texture == null) 422 | continue; 423 | 424 | if (it.Tile == null) 425 | continue; 426 | 427 | var position = new Vector2(x, y) * Game.TileSize + it.Offset; 428 | batcher.Image(it.Image, position, it.Origin, Vector2.One, 0, Color.White); 429 | } 430 | 431 | // draw "actors" second 432 | for (int x = 0; x < Game.Columns; x ++) 433 | for (int y = 0; y < Game.Rows; y ++) 434 | { 435 | if (Factory.Find(editing.Tiles[x, y]) is not {} it || it.Image.Texture == null) 436 | continue; 437 | 438 | if (it.Spawn == null) 439 | continue; 440 | 441 | var position = new Vector2(x, y) * Game.TileSize + it.Offset; 442 | batcher.Image(it.Image, position, it.Origin, Vector2.One, 0, Color.White); 443 | } 444 | 445 | // draw grid 446 | for (int x = 1; x < Game.Columns; x ++) 447 | batcher.Line(new Vector2(x* Game.TileSize, 0), new Vector2(x* Game.TileSize, Game.Height), gridWeight, gridColor); 448 | for (int y = 1; y < Game.Rows; y ++) 449 | batcher.Line(new Vector2(0, y * Game.TileSize), new Vector2(Game.Width, y * Game.TileSize), gridWeight, gridColor); 450 | 451 | // begin placing tiles 452 | if (lastCell == cell && overGame && (Input.Mouse.LeftPressed || Input.Mouse.RightPressed)) 453 | { 454 | tilePlaceFrom = tileOver; 455 | tileTypePlacing = Input.Mouse.LeftPressed ? brush : '0'; 456 | tilePlacing = true; 457 | } 458 | 459 | // return tile placing to selected brush 460 | if (!Input.Mouse.RightDown) 461 | tileTypePlacing = brush; 462 | 463 | // change shift/size based opn big brush 464 | var shift = brushBig ? -1 : 0; 465 | var size = brushBig ? 3 : 1; 466 | 467 | // draw cursor 468 | if (overGame) 469 | { 470 | var cursorRect = (new Rect(tileOver.X + shift, tileOver.Y + shift, size, size) * Game.TileSize).Inflate(gridWeight * 2); 471 | var cursorColor = tileTypePlacing == '0' ? Color.Red : highlight; 472 | batcher.RectLine(cursorRect, gridWeight * 2, cursorColor); 473 | 474 | if (Factory.Find(brush) is {} placing) 475 | { 476 | var position = tileOver * Game.TileSize + placing.Offset; 477 | 478 | for (int x = shift; x < shift + size; x ++) 479 | for (int y = shift; y < shift + size; y ++) 480 | batcher.Image(placing.Image, position + new Point2(x, y) * Game.TileSize, placing.Origin, Vector2.One, 0, Color.White); 481 | } 482 | } 483 | 484 | // place tiles 485 | if (tilePlacing && (Input.Mouse.LeftDown || Input.Mouse.RightDown)) 486 | { 487 | editing.Set(tilePlaceFrom, tileTypePlacing); 488 | 489 | while (tilePlaceFrom != tileOver) 490 | { 491 | var diff = tileOver - tilePlaceFrom; 492 | if (Math.Abs(diff.X) > Math.Abs(diff.Y)) 493 | diff.Y = 0; 494 | else 495 | diff.X = 0; 496 | tilePlaceFrom.X += Math.Sign(diff.X); 497 | tilePlaceFrom.Y += Math.Sign(diff.Y); 498 | 499 | for (int x = shift; x < shift + size; x ++) 500 | for (int y = shift; y < shift + size; y ++) 501 | editing.Set(tilePlaceFrom + new Point2(x, y), tileTypePlacing); 502 | } 503 | } 504 | 505 | batcher.PopScissor(); 506 | batcher.RectLine(new Rect(0, 0, Game.Width, Game.Height), LineWeight, Color.Black); 507 | batcher.PopMatrix(); 508 | } 509 | else 510 | { 511 | if (Button(arrow + bounds.Center, Assets.GetSubtexture("buttons/2"), "Create New Room Here")) 512 | { 513 | var it = new Room(cell); 514 | Assets.Rooms.Add(cell, it); 515 | it.Save(); 516 | } 517 | } 518 | 519 | if (!Input.Mouse.LeftDown && !Input.Mouse.RightDown) 520 | { 521 | tileTypePlacing = brush; 522 | tilePlacing = false; 523 | } 524 | } 525 | } 526 | -------------------------------------------------------------------------------- /TinyLink/Source/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FosterFramework/Samples/5b97ca5329e61768f85a45d655da5df7f882519d/TinyLink/Source/screenshot.png -------------------------------------------------------------------------------- /TinyLink/TinyLink.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | enable 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /TinyLink/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FosterFramework/Samples/5b97ca5329e61768f85a45d655da5df7f882519d/TinyLink/screenshot.png --------------------------------------------------------------------------------