├── .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 | 
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 | 
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 | 
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 | 
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
--------------------------------------------------------------------------------