commit dbbe203db8386b047cddcd97175f8dd6297835b3 Author: MasterGordon Date: Sun Oct 9 22:58:45 2022 +0200 init diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 0000000..7beca10 --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,32 @@ +name: .NET + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 6.0.x + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore + - name: Publish + run: dotnet publish -r linux-x64 + - name: Upload a Build Artifact + uses: actions/upload-artifact@v3.1.0 + with: + # Artifact name + name: Linux build + # A file, directory or wildcard pattern that describes what to upload + path: bin/Debug/net6.0/linux-x64/publish/asdlteroids diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cd42ee3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +bin/ +obj/ diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..c0f007e --- /dev/null +++ b/Program.cs @@ -0,0 +1,2 @@ +var boulderDash = new BoulderDash(); +boulderDash.Run(); diff --git a/assets/explode.wav b/assets/explode.wav new file mode 100644 index 0000000..0741f66 Binary files /dev/null and b/assets/explode.wav differ diff --git a/assets/font.ttf b/assets/font.ttf new file mode 100644 index 0000000..39adf42 Binary files /dev/null and b/assets/font.ttf differ diff --git a/assets/level1.json b/assets/level1.json new file mode 100644 index 0000000..85e9d91 --- /dev/null +++ b/assets/level1.json @@ -0,0 +1,18 @@ +[ + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] +] diff --git a/assets/shot.wav b/assets/shot.wav new file mode 100644 index 0000000..7de2c5f Binary files /dev/null and b/assets/shot.wav differ diff --git a/assets/sprites.png b/assets/sprites.png new file mode 100644 index 0000000..d83727a Binary files /dev/null and b/assets/sprites.png differ diff --git a/boulder-dash.csproj b/boulder-dash.csproj new file mode 100644 index 0000000..45b0e45 --- /dev/null +++ b/boulder-dash.csproj @@ -0,0 +1,22 @@ + + + + Exe + net6.0 + boulder-dash + enable + true + true + true + enable + + + + + + + + + + + diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..f95b8f4 --- /dev/null +++ b/readme.md @@ -0,0 +1,2 @@ +# Boulder Dash SDL + diff --git a/src/BoulderDash.cs b/src/BoulderDash.cs new file mode 100644 index 0000000..6ee57f4 --- /dev/null +++ b/src/BoulderDash.cs @@ -0,0 +1,60 @@ +using static SDL2.SDL; + +class BoulderDash : Game +{ + private Context context; + private TileSet tileSet; + + public BoulderDash() + { + var window = new Window("Boulder Dash", 800, 600); + var renderer = new Renderer(window); + this.registerScenes(); + this.context = new Context(window, renderer); + GameState.sceneManager.ChangeScene("main", context); + this.tileSet = new TileSet(context, "assets.sprites.png", 32); + } + + private void registerScenes() + { + GameState.sceneManager.AddScene("main", new MainMenu()); + } + + protected override void update(double dt) + { + SDL_Event e; + while (SDL_PollEvent(out e) != 0) + { + if (e.type == SDL_EventType.SDL_QUIT) + { + System.Environment.Exit(0); + } + if (e.type == SDL_EventType.SDL_KEYDOWN) + { + if (e.key.keysym.sym == SDL_Keycode.SDLK_ESCAPE) + { + System.Environment.Exit(0); + } + } + } + GameState.keyState = new KeyState(); + GameState.sceneManager.GetCurrentScene()?.Update(dt, context); + } + + protected override void draw() + { +#if (DEBUG) + SDL_SetWindowTitle(this.context.window.GetWindow(), "Boulder Dash - " + GameState.fps + "FPS"); +#endif + this.context.renderer.Clear(); + GameState.sceneManager.GetCurrentScene()?.Draw(this.context); + this.context.renderer.DrawTileSet(this.tileSet, 0, 0, 1, 6); + this.context.renderer.DrawTileSet(this.tileSet, 32, 0, 1, 6); + this.context.renderer.DrawTileSet(this.tileSet, 64, 0, 1, 6); + this.context.renderer.DrawTileSet(this.tileSet, 96, 0, 1, 6); + this.context.renderer.DrawTileSet(this.tileSet, 128, 0, 1, 6); + this.context.renderer.DrawTileSet(this.tileSet, 160, 0, 1, 6); + this.context.renderer.Present(); + SDL_Delay(1); + } +} diff --git a/src/Controls.cs b/src/Controls.cs new file mode 100644 index 0000000..8bf1812 --- /dev/null +++ b/src/Controls.cs @@ -0,0 +1,32 @@ +using static SDL2.SDL; + +enum Control +{ + UP, + DOWN, + LEFT, + RIGHT, + STAY, +} + +static class ControlKeyExtension +{ + public static SDL_Keycode Key(this Control c) + { + switch (c) + { + case Control.UP: + return SDL_Keycode.SDLK_w; + case Control.DOWN: + return SDL_Keycode.SDLK_s; + case Control.LEFT: + return SDL_Keycode.SDLK_a; + case Control.RIGHT: + return SDL_Keycode.SDLK_d; + case Control.STAY: + return SDL_Keycode.SDLK_LCTRL; + default: + throw new ArgumentException("Invalid control"); + } + } +} diff --git a/src/actors/Map.cs b/src/actors/Map.cs new file mode 100644 index 0000000..0f3b176 --- /dev/null +++ b/src/actors/Map.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; + +class Map : Actor +{ + private int[][] map; + + public Map(uint width, uint height) + { + this.map = new int[height][]; + for (uint i = 0; i < height; i++) + { + this.map[i] = new int[width]; + } + } + + public Map(string json) + { + var map = JsonConvert.DeserializeObject(json); + if (map == null) + { + throw new Exception("Invalid map"); + } + this.map = map; + } +} diff --git a/src/engine/Actor.cs b/src/engine/Actor.cs new file mode 100644 index 0000000..e38f6fd --- /dev/null +++ b/src/engine/Actor.cs @@ -0,0 +1,12 @@ +abstract class Actor +{ + public virtual void Update(double dt, Context context) + { + + } + + public virtual void Draw(Context context) + { + + } +} diff --git a/src/engine/AudioPlayer.cs b/src/engine/AudioPlayer.cs new file mode 100644 index 0000000..dec199e --- /dev/null +++ b/src/engine/AudioPlayer.cs @@ -0,0 +1,27 @@ +enum Sound +{ +} + +class AudioPlayer +{ + private Dictionary audioFiles = new(); + private ResourceLoader resourceLoader = new(); + + public AudioPlayer() + { + SDL2.SDL_mixer.Mix_OpenAudio(44100, SDL2.SDL_mixer.MIX_DEFAULT_FORMAT, 2, 2048); + } + + public void Register(Sound name, string path) + { + var buffer = resourceLoader.LoadBytes(path); + this.audioFiles.Add(name, buffer); + } + + public void Play(Sound name) + { + var buffer = this.audioFiles[name]; + var sound = SDL2.SDL_mixer.Mix_QuickLoad_WAV(buffer); + SDL2.SDL_mixer.Mix_PlayChannel((int)name, sound, 0); + } +} diff --git a/src/engine/Context.cs b/src/engine/Context.cs new file mode 100644 index 0000000..516ab10 --- /dev/null +++ b/src/engine/Context.cs @@ -0,0 +1,15 @@ +struct Context +{ + public Context(Window window, Renderer renderer) + { + this.window = window; + this.renderer = renderer; + this.resourceLoader = new ResourceLoader(); + this.fontManager = new FontManager(resourceLoader); + } + + public Window window { get; } + public Renderer renderer { get; } + public ResourceLoader resourceLoader { get; } + public FontManager fontManager { get; } +} diff --git a/src/engine/FontManager.cs b/src/engine/FontManager.cs new file mode 100644 index 0000000..ffcbbb1 --- /dev/null +++ b/src/engine/FontManager.cs @@ -0,0 +1,35 @@ +using static SDL2.SDL_ttf; +using static SDL2.SDL; + +class FontManager +{ + private Dictionary fonts = new(); + private ResourceLoader resourceLoader; + + public FontManager(ResourceLoader resourceLoader) + { + this.resourceLoader = resourceLoader; + if (TTF_Init() != 0) + { + throw new Exception("TTF_Init failed"); + } + } + + public void RegisterFont(string name, string path, int fontSize) + { + if (fonts.ContainsKey(name)) return; + var res = resourceLoader.LoadToIntPtr(path); + var sdlBuffer = SDL_RWFromConstMem(res.ptr, res.size); + var font = TTF_OpenFontRW(sdlBuffer, 1, fontSize); + if (font == IntPtr.Zero) + { + throw new Exception("TTF_OpenFont failed"); + } + fonts.Add(name, font); + } + + public IntPtr GetFont(string name) + { + return fonts[name]; + } +} diff --git a/src/engine/Game.cs b/src/engine/Game.cs new file mode 100644 index 0000000..27fd567 --- /dev/null +++ b/src/engine/Game.cs @@ -0,0 +1,31 @@ +abstract class Game +{ + public const int TPS = 64; + private Queue fpsQueue = new Queue(); + protected abstract void update(double dt); + protected abstract void draw(); + + public void Run() + { + var tLast = DateTime.Now; + var tAcc = TimeSpan.Zero; + while (true) + { + var dt = DateTime.Now - tLast; + tLast = DateTime.Now; + tAcc += dt; + var fps = (int)(1 / dt.TotalSeconds); + fpsQueue.Enqueue(fps); + while (fpsQueue.Count > fps) + fpsQueue.Dequeue(); + GameState.fps = (int)fpsQueue.Average(); + while (tAcc >= TimeSpan.FromSeconds(1.0 / TPS)) + { + update(dt.TotalSeconds); + tAcc -= TimeSpan.FromSeconds(1.0 / TPS); + } + + draw(); + } + } +} diff --git a/src/engine/GameState.cs b/src/engine/GameState.cs new file mode 100644 index 0000000..0acdfd7 --- /dev/null +++ b/src/engine/GameState.cs @@ -0,0 +1,6 @@ +static class GameState +{ + public static KeyState keyState = new KeyState(); + public static SceneManager sceneManager = new SceneManager(); + public static int fps = 0; +} diff --git a/src/engine/KeyState.cs b/src/engine/KeyState.cs new file mode 100644 index 0000000..71ffa07 --- /dev/null +++ b/src/engine/KeyState.cs @@ -0,0 +1,25 @@ +using System.Runtime.InteropServices; +using static SDL2.SDL; + +class KeyState +{ + private byte[] keys; + + public KeyState() + { + var origArray = SDL_GetKeyboardState(out var arraySize); + this.keys = new byte[arraySize]; + Marshal.Copy(origArray, this.keys, 0, arraySize); + } + + public bool isPressed(SDL_Keycode keycode) + { + byte scanCode = (byte)SDL_GetScancodeFromKey(keycode); + return (this.keys[scanCode] == 1); + } + + public bool isPressed(Control c) + { + return this.isPressed(c.Key()); + } +} diff --git a/src/engine/Renderer.cs b/src/engine/Renderer.cs new file mode 100644 index 0000000..dc19ddd --- /dev/null +++ b/src/engine/Renderer.cs @@ -0,0 +1,107 @@ +using static SDL2.SDL; +using static SDL2.SDL_ttf; + +class Renderer +{ + private IntPtr renderer; + private IntPtr font; + private SDL_Color color; + + public Renderer(Window window) + { + this.renderer = SDL_CreateRenderer(window.GetWindow(), -1, SDL_RendererFlags.SDL_RENDERER_ACCELERATED); + } + + public void Clear() + { + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); + SDL_RenderClear(renderer); + } + + public void Present() + { + SDL_RenderPresent(renderer); + } + + public void DrawRect(double x, double y, int w, int h) + { + this.DrawRect((int)x, (int)y, w, h); + } + + public void DrawRect(int x, int y, int w, int h) + { + SDL_Rect rect = new SDL_Rect(); + rect.x = x; + rect.y = y; + rect.w = w; + rect.h = h; + + SDL_RenderFillRect(renderer, ref rect); + } + + public void DrawLines(double[][] points) + { + SDL_Point[] sdlPoints = new SDL_Point[points.Length]; + for (int i = 0; i < points.Length; i++) + { + sdlPoints[i].x = (int)points[i][0]; + sdlPoints[i].y = (int)points[i][1]; + } + + SDL_RenderDrawLines(renderer, sdlPoints, points.Length); + } + + public void SetColor(int r, int g, int b, int a = 255) + { + SDL_SetRenderDrawColor(renderer, (byte)r, (byte)g, (byte)b, (byte)a); + } + + public void SetFont(IntPtr font, SDL_Color color) + { + this.font = font; + this.color = color; + } + + public void SetFont(IntPtr font, Color color) + { + this.font = font; + this.color = color.toSDLColor(); + } + + public void DrawText(string text, int x, int y, bool center = false) + { + var surfaceMessage = TTF_RenderText_Solid(this.font, text, this.color); + + var texture = SDL_CreateTextureFromSurface(this.renderer, surfaceMessage); + int width; + int height; + + SDL_QueryTexture(texture, out _, out _, out width, out height); + + SDL_Rect rect = new SDL_Rect(); + rect.x = x; + rect.y = y; + rect.w = width; + rect.h = height; + + if (center) + { + rect.x -= width / 2; + rect.y -= height / 2; + } + + SDL_RenderCopy(this.renderer, texture, IntPtr.Zero, ref rect); + SDL_DestroyTexture(texture); + SDL_FreeSurface(surfaceMessage); + } + + public IntPtr GetRaw() + { + return renderer; + } + + public IntPtr CreateTextureFromSurface(IntPtr surface) + { + return SDL_CreateTextureFromSurface(renderer, surface); + } +} diff --git a/src/engine/ResourceLoader.cs b/src/engine/ResourceLoader.cs new file mode 100644 index 0000000..f4b0077 --- /dev/null +++ b/src/engine/ResourceLoader.cs @@ -0,0 +1,34 @@ +using System.Runtime.InteropServices; + +class ResourceLoader +{ + private string assemblyName; + + public ResourceLoader() + { + this.assemblyName = this.GetType().Assembly.GetName().Name!; + } + + public string LoadString(string resourceName) + { + using var stream = this.GetType().Assembly.GetManifestResourceStream($"{this.assemblyName}.{resourceName}"); + using var reader = new StreamReader(stream!); + return reader.ReadToEnd(); + } + + public byte[] LoadBytes(string resourceName) + { + using var stream = this.GetType().Assembly.GetManifestResourceStream($"{this.assemblyName}.{resourceName}"); + using var memoryStream = new MemoryStream(); + stream!.CopyTo(memoryStream); + return memoryStream.ToArray(); + } + + public (IntPtr ptr, int size) LoadToIntPtr(string resourceName) + { + var bytes = this.LoadBytes(resourceName); + var handle = GCHandle.Alloc(bytes, GCHandleType.Pinned); + var ptr = handle.AddrOfPinnedObject(); + return (ptr, bytes.Length); + } +} diff --git a/src/engine/Scene.cs b/src/engine/Scene.cs new file mode 100644 index 0000000..740a890 --- /dev/null +++ b/src/engine/Scene.cs @@ -0,0 +1,12 @@ +abstract class Scene : Actor +{ + public virtual void Create(Context context) + { + + } + + public virtual void Destroy(Context context) + { + + } +} diff --git a/src/engine/SceneManager.cs b/src/engine/SceneManager.cs new file mode 100644 index 0000000..67551b6 --- /dev/null +++ b/src/engine/SceneManager.cs @@ -0,0 +1,26 @@ +class SceneManager +{ + private Dictionary scenes = new Dictionary(); + private Scene? currentScene; + + public void AddScene(string name, Scene scene) + { + scenes.Add(name, scene); + } + + public void ChangeScene(string name, Context context) + { + if (currentScene != null) + { + currentScene.Destroy(context); + } + + currentScene = scenes[name]; + currentScene.Create(context); + } + + public Scene? GetCurrentScene() + { + return currentScene; + } +} diff --git a/src/engine/TileSet.cs b/src/engine/TileSet.cs new file mode 100644 index 0000000..de7290b --- /dev/null +++ b/src/engine/TileSet.cs @@ -0,0 +1,46 @@ +using static SDL2.SDL_image; +using static SDL2.SDL; + +class TileSet +{ + public readonly IntPtr Texture; + public readonly int Width, Height, Resolution; + + public TileSet(Context context, String tileSetPath, int resolution) + { + var res = context.resourceLoader.LoadToIntPtr(tileSetPath); + var sdlBuffer = SDL_RWFromMem(res.ptr, res.size); + var surface = IMG_Load_RW(sdlBuffer, 1); + this.Texture = context.renderer.CreateTextureFromSurface(surface); + SDL_QueryTexture(Texture, out _, out _, out Width, out Height); + SDL_FreeSurface(surface); + this.Resolution = resolution; + } + + ~TileSet() + { + SDL_DestroyTexture(Texture); + } +} + +static class TileSetRendererExtension +{ + public static void DrawTileSet(this Renderer renderer, TileSet tileSet, int x, int y, int tileX, int tileY) + { + var src = new SDL_Rect + { + x = tileX * tileSet.Resolution, + y = tileY * tileSet.Resolution, + w = tileSet.Resolution, + h = tileSet.Resolution + }; + var dst = new SDL_Rect + { + x = x, + y = y, + w = tileSet.Resolution, + h = tileSet.Resolution + }; + SDL_RenderCopy(renderer.GetRaw(), tileSet.Texture, ref src, ref dst); + } +} diff --git a/src/engine/Window.cs b/src/engine/Window.cs new file mode 100644 index 0000000..1f6c79c --- /dev/null +++ b/src/engine/Window.cs @@ -0,0 +1,33 @@ +using static SDL2.SDL; + +class Window +{ + IntPtr window; + + public Window(string title, int w, int h) + { + window = SDL_CreateWindow(title, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, w, h, SDL_WindowFlags.SDL_WINDOW_VULKAN | SDL_WindowFlags.SDL_WINDOW_RESIZABLE); + } + + public Window(string title, int x, int y, int w, int h, SDL_WindowFlags flags) + { + this.window = SDL_CreateWindow(title, x, y, w, h, flags); + } + + public void Dispose() + { + SDL_DestroyWindow(this.window); + } + + public IntPtr GetWindow() + { + return this.window; + } + + public (int width, int height) GetSize() + { + int w, h; + SDL_GetWindowSize(this.window, out w, out h); + return (w, h); + } +} diff --git a/src/engine/interfaces/Logic.cs b/src/engine/interfaces/Logic.cs new file mode 100644 index 0000000..7db6d41 --- /dev/null +++ b/src/engine/interfaces/Logic.cs @@ -0,0 +1,4 @@ +interface Logic +{ + public void Update(KeyState keyState, double dx); +} diff --git a/src/engine/interfaces/Renderable.cs b/src/engine/interfaces/Renderable.cs new file mode 100644 index 0000000..7a0c1aa --- /dev/null +++ b/src/engine/interfaces/Renderable.cs @@ -0,0 +1,4 @@ +interface Renderable +{ + public void Render(Renderer renderer, double dx); +} diff --git a/src/engine/utils/Color.cs b/src/engine/utils/Color.cs new file mode 100644 index 0000000..7d82521 --- /dev/null +++ b/src/engine/utils/Color.cs @@ -0,0 +1,32 @@ +using static SDL2.SDL; + +class Color +{ + int r, g, b, a; + + public Color(int r, int g, int b, int a) + { + this.r = r; + this.g = g; + this.b = b; + this.a = a; + } + + public Color(int r, int g, int b) + { + this.r = r; + this.g = g; + this.b = b; + this.a = 255; + } + + public SDL_Color toSDLColor() + { + SDL_Color color = new SDL_Color(); + color.r = (byte)r; + color.g = (byte)g; + color.b = (byte)b; + color.a = (byte)a; + return color; + } +} diff --git a/src/engine/utils/Point.cs b/src/engine/utils/Point.cs new file mode 100644 index 0000000..585457e --- /dev/null +++ b/src/engine/utils/Point.cs @@ -0,0 +1,7 @@ +class Point +{ + public static double Distance(double x1, double y1, double x2, double y2) + { + return Math.Sqrt(Math.Pow(x2 - x1, 2) + Math.Pow(y2 - y1, 2)); + } +} diff --git a/src/scenes/MainMenu.cs b/src/scenes/MainMenu.cs new file mode 100644 index 0000000..2eba468 --- /dev/null +++ b/src/scenes/MainMenu.cs @@ -0,0 +1,33 @@ +class MainMenu : Scene +{ + double x = 0; + bool dir = true; + public override void Create(Context context) + { + context.fontManager.RegisterFont("MainMenu", "assets.font.ttf", 24); + } + + public override void Update(double dt, Context context) + { + x += dir ? 10 : -10; + var windowSize = context.window.GetSize(); + if (x + 30 > windowSize.width) + { + dir = false; + } + if (x < 0) + { + dir = true; + } + } + + public override void Draw(Context context) + { + var windowSize = context.window.GetSize(); + var font = context.fontManager.GetFont("MainMenu"); + context.renderer.SetFont(font, new Color(255, 255, 255)); + context.renderer.DrawText("Main Menu", windowSize.width / 2, windowSize.height / 2, true); + context.renderer.SetColor(255, 255, 255); + context.renderer.DrawRect(x, 0, 30, 30); + } +}