From 69596ab172deba58eb367a8306a81850e8f503d7 Mon Sep 17 00:00:00 2001 From: MasterGordon Date: Sat, 5 Nov 2022 12:59:45 +0100 Subject: [PATCH] first commit --- .editorconfig | 71 ++++++++++ .github/workflows/dotnet.yml | 32 +++++ .gitignore | 2 + Program.cs | 13 ++ assets/font.ttf | Bin 0 -> 118204 bytes assets/stone.png | Bin 0 -> 454 bytes mine2d.csproj | 24 ++++ readme.md | 2 + src/Context.cs | 45 +++++++ src/Controls.cs | 35 +++++ src/Import.cs | 4 + src/Mine2d.cs | 46 +++++++ src/backend/Backend.cs | 87 ++++++++++++ src/backend/IBackend.cs | 6 + src/backend/Publisher.cs | 86 ++++++++++++ src/backend/RemoteBackend.cs | 52 ++++++++ src/backend/data/Packet.cs | 49 +++++++ src/backend/interactor/Connect.cs | 22 +++ src/backend/interactor/Move.cs | 29 ++++ src/core/Bootstrapper.cs | 12 ++ src/core/Camera.cs | 19 +++ src/core/Constants.cs | 6 + src/core/PlayerEntity.cs | 69 ++++++++++ src/core/data/Chunk.cs | 63 +++++++++ src/core/data/World.cs | 71 ++++++++++ src/core/tiles/Tile.cs | 48 +++++++ src/core/tiles/TileRegistry.cs | 19 +++ src/core/world/ChunkGenerator.cs | 20 +++ src/engine/AudioPlayer.cs | 25 ++++ src/engine/FontManager.cs | 36 +++++ src/engine/Game.cs | 30 +++++ src/engine/PacketUtils.cs | 22 +++ src/engine/Renderer.cs | 120 +++++++++++++++++ src/engine/ResourceLoader.cs | 52 ++++++++ src/engine/Shapes.cs | 11 ++ src/engine/Window.cs | 44 ++++++ src/engine/system/EventPriority.cs | 9 ++ src/engine/system/annotations/Interactor.cs | 22 +++ src/engine/utils/Color.cs | 33 +++++ src/engine/utils/Point.cs | 7 + src/frontend/Frontend.cs | 141 ++++++++++++++++++++ src/frontend/IFrontend.cs | 5 + src/frontend/renderer/IRenderer.cs | 4 + src/frontend/renderer/WorldRenderer.cs | 24 ++++ src/network/Converter.cs | 36 +++++ src/state/GameState.cs | 23 ++++ src/state/Player.cs | 12 ++ 47 files changed, 1588 insertions(+) create mode 100644 .editorconfig create mode 100644 .github/workflows/dotnet.yml create mode 100644 .gitignore create mode 100644 Program.cs create mode 100644 assets/font.ttf create mode 100644 assets/stone.png create mode 100644 mine2d.csproj create mode 100644 readme.md create mode 100644 src/Context.cs create mode 100644 src/Controls.cs create mode 100644 src/Import.cs create mode 100644 src/Mine2d.cs create mode 100644 src/backend/Backend.cs create mode 100644 src/backend/IBackend.cs create mode 100644 src/backend/Publisher.cs create mode 100644 src/backend/RemoteBackend.cs create mode 100644 src/backend/data/Packet.cs create mode 100644 src/backend/interactor/Connect.cs create mode 100644 src/backend/interactor/Move.cs create mode 100644 src/core/Bootstrapper.cs create mode 100644 src/core/Camera.cs create mode 100644 src/core/Constants.cs create mode 100644 src/core/PlayerEntity.cs create mode 100644 src/core/data/Chunk.cs create mode 100644 src/core/data/World.cs create mode 100644 src/core/tiles/Tile.cs create mode 100644 src/core/tiles/TileRegistry.cs create mode 100644 src/core/world/ChunkGenerator.cs create mode 100644 src/engine/AudioPlayer.cs create mode 100644 src/engine/FontManager.cs create mode 100644 src/engine/Game.cs create mode 100644 src/engine/PacketUtils.cs create mode 100644 src/engine/Renderer.cs create mode 100644 src/engine/ResourceLoader.cs create mode 100644 src/engine/Shapes.cs create mode 100644 src/engine/Window.cs create mode 100644 src/engine/system/EventPriority.cs create mode 100644 src/engine/system/annotations/Interactor.cs create mode 100644 src/engine/utils/Color.cs create mode 100644 src/engine/utils/Point.cs create mode 100644 src/frontend/Frontend.cs create mode 100644 src/frontend/IFrontend.cs create mode 100644 src/frontend/renderer/IRenderer.cs create mode 100644 src/frontend/renderer/WorldRenderer.cs create mode 100644 src/network/Converter.cs create mode 100644 src/state/GameState.cs create mode 100644 src/state/Player.cs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e2c242d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,71 @@ +[*.{appxmanifest,asax,ascx,aspx,axaml,build,c,c++,cc,cginc,compute,cp,cpp,cs,cshtml,cu,cuh,cxx,dtd,fs,fsi,fsscript,fsx,fx,fxh,h,hh,hlsl,hlsli,hlslinc,hpp,hxx,inc,inl,ino,ipp,master,ml,mli,mpp,mq4,mq5,mqh,nuspec,paml,razor,resw,resx,shader,skin,tpp,usf,ush,vb,xaml,xamlx,xoml,xsd}] +indent_style = space +indent_size = 4 +tab_width = 4 + +[*] + +# Microsoft .NET properties +csharp_new_line_before_members_in_object_initializers = false +csharp_preferred_modifier_order = public, private, protected, internal, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async, required:suggestion +csharp_style_var_elsewhere = true:suggestion +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +dotnet_naming_rule.private_constants_rule.import_to_resharper = as_predefined +dotnet_naming_rule.private_constants_rule.severity = warning +dotnet_naming_rule.private_constants_rule.style = upper_camel_case_style +dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols +dotnet_naming_rule.private_instance_fields_rule.import_to_resharper = as_predefined +dotnet_naming_rule.private_instance_fields_rule.severity = warning +dotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style +dotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_fields_symbols +dotnet_naming_rule.private_static_fields_rule.import_to_resharper = as_predefined +dotnet_naming_rule.private_static_fields_rule.severity = warning +dotnet_naming_rule.private_static_fields_rule.style = lower_camel_case_style_1 +dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols +dotnet_naming_rule.private_static_readonly_rule.import_to_resharper = as_predefined +dotnet_naming_rule.private_static_readonly_rule.severity = warning +dotnet_naming_rule.private_static_readonly_rule.style = upper_camel_case_style +dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols +dotnet_naming_rule.unity_serialized_field_rule.import_to_resharper = True +dotnet_naming_rule.unity_serialized_field_rule.resharper_description = Unity serialized field +dotnet_naming_rule.unity_serialized_field_rule.resharper_guid = 5f0fdb63-c892-4d2c-9324-15c80b22a7ef +dotnet_naming_rule.unity_serialized_field_rule.severity = warning +dotnet_naming_rule.unity_serialized_field_rule.style = lower_camel_case_style +dotnet_naming_rule.unity_serialized_field_rule.symbols = unity_serialized_field_symbols +dotnet_naming_style.lower_camel_case_style.capitalization = camel_case +dotnet_naming_style.lower_camel_case_style_1.capitalization = camel_case +dotnet_naming_style.lower_camel_case_style_1.required_prefix = _ +dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case +dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field +dotnet_naming_symbols.private_constants_symbols.required_modifiers = const +dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static +dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static,readonly +dotnet_naming_symbols.unity_serialized_field_symbols.applicable_accessibilities = * +dotnet_naming_symbols.unity_serialized_field_symbols.applicable_kinds = +dotnet_naming_symbols.unity_serialized_field_symbols.resharper_applicable_kinds = unity_serialised_field +dotnet_naming_symbols.unity_serialized_field_symbols.resharper_required_modifiers = instance +dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none +dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion +dotnet_style_qualification_for_event = false:suggestion +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion + +dotnet_style_qualification_for_field = true:warning +dotnet_style_qualification_for_method = true:warning +dotnet_style_qualification_for_property = true:warning +dotnet_style_qualification_for_event = true:warning +csharp_style_unused_value_expression_statement_preference = false +dotnet_diagnostic.CA1805.severity = none 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..2c3e067 --- /dev/null +++ b/Program.cs @@ -0,0 +1,13 @@ +class Program +{ + static void Main(string[] args) + { + bool isHost = args.Contains("--host"); + // bool isHost = true; + var game = new Mine2d(isHost); + game.Run(); + // var p = new Publisher(isHost ? InteractorKind.Server : InteractorKind.Client); + // p.Dump(); + // Console.WriteLine("Hello World!"); + } +} diff --git a/assets/font.ttf b/assets/font.ttf new file mode 100644 index 0000000000000000000000000000000000000000..39adf42efa597906e53be689474ac82214112124 GIT binary patch literal 118204 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 Ap8x;= literal 0 HcmV?d00001 diff --git a/assets/stone.png b/assets/stone.png new file mode 100644 index 0000000000000000000000000000000000000000..496c4acaf66689e73c6fcd8ac42308362d7c2185 GIT binary patch literal 454 zcmV;%0XhDOP)Px$fJsC_R5*=wlWUHoFbsr0KM39G!>u&Z-2W6UZ6LAtM+nd}+DHiFq{?MikngWQ zS=N=g-tRZ-y5jD%)~L1ed|dl;i7EgN5E1B+c)eZ#wAKLr3(g1t)R%=x@^h&`BEP}6 zoX;SlUqDqLi4-76pw04}3t|hsH_V*Y`qAbfPXvwVWP%WsGA!+R z*_0WSQZQ2B%wh-Wu+Xnc_k)Nlfyi`ke$o)F@@# zOc4PAO_aky0y_^+!u$6m5jB>h-E(&w9AZu79Hw%+p|^P7N9=M_>^=rF{v$g9^BqSn wFjNu{+}(&sR6l#4fIETG$9>s1%!l>=0Pb_Q3Y|10r2qf`07*qoM6N<$g5Vs)od5s; literal 0 HcmV?d00001 diff --git a/mine2d.csproj b/mine2d.csproj new file mode 100644 index 0000000..140033f --- /dev/null +++ b/mine2d.csproj @@ -0,0 +1,24 @@ + + + + Exe + net6.0 + mine2d + enable + true + link + true + true + disable + + + + + + + + + + + + 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/Context.cs b/src/Context.cs new file mode 100644 index 0000000..7f40153 --- /dev/null +++ b/src/Context.cs @@ -0,0 +1,45 @@ +class Context +{ + public bool IsHost { get; set; } + public IBackend Backend { get; set; } + public IFrontend Frontend { get; set; } + public GameState GameState { get; set; } + public FrontendGameState FrontendGameState { get; set; } + public Window Window { get; set; } + public Renderer Renderer { get; set; } + public TileRegistry TileRegistry { get; set; } + public ResourceLoader ResourceLoader { get; set; } + public static Context instance { get; set; } + + public Context( + bool isHost, + IBackend backend, + IFrontend frontend, + GameState gameState, + FrontendGameState frontendGameState, + Renderer renderer, + Window window + ) + { + this.IsHost = isHost; + this.Backend = backend; + this.Frontend = frontend; + this.GameState = gameState; + this.FrontendGameState = frontendGameState; + this.Renderer = renderer; + this.Window = window; + this.TileRegistry = new TileRegistry(); + this.ResourceLoader = new ResourceLoader(); + Context.instance = this; + } + + public static Context Get() + { + if (Context.instance == null) + { + throw new Exception("Context not initialized"); + } + + return Context.instance; + } +} diff --git a/src/Controls.cs b/src/Controls.cs new file mode 100644 index 0000000..9ffc27a --- /dev/null +++ b/src/Controls.cs @@ -0,0 +1,35 @@ +using static SDL2.SDL; + +enum Control +{ + UP, + DOWN, + LEFT, + RIGHT, + STAY, + CONFIRM, +} + +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; + case Control.CONFIRM: + return SDL_Keycode.SDLK_SPACE; + default: + throw new ArgumentException("Invalid control"); + } + } +} diff --git a/src/Import.cs b/src/Import.cs new file mode 100644 index 0000000..1dd9f3a --- /dev/null +++ b/src/Import.cs @@ -0,0 +1,4 @@ +global using System.Numerics; +global using static SDL2.SDL; +global using static SDL2.SDL_image; +global using static SDL2.SDL_ttf; diff --git a/src/Mine2d.cs b/src/Mine2d.cs new file mode 100644 index 0000000..43ab454 --- /dev/null +++ b/src/Mine2d.cs @@ -0,0 +1,46 @@ +class Mine2d : Game +{ + private Context ctx; + + public Mine2d(bool isHost) + { + var window = new Window("MultiPlayerGame" + (isHost ? " - host" : ""), 1200, 800); + if (isHost) + { + this.ctx = new Context( + isHost, + new Backend(), + new Frontend(), + new GameState(), + new FrontendGameState(), + new Renderer(window), + window + ); + } + else + { + this.ctx = new Context( + isHost, + new RemoteBackend(), + new Frontend(), + new GameState(), + new FrontendGameState(), + new Renderer(window), + window + ); + } + Bootstrapper.Bootstrap(); + ctx.Backend.Init(); + ctx.Frontend.Init(); + } + + protected override void draw() + { + ctx.Frontend.Process(); + } + + protected override void update(double dt) + { + ctx.Backend.Process(dt); + } +} diff --git a/src/backend/Backend.cs b/src/backend/Backend.cs new file mode 100644 index 0000000..400fdf0 --- /dev/null +++ b/src/backend/Backend.cs @@ -0,0 +1,87 @@ +using System.Text; +using Newtonsoft.Json; +using WatsonTcp; + +class Backend : IBackend +{ + private WatsonTcpServer server; + private Publisher publisher; + private Queue pendingPackets = new(); + private uint tick = 0; + + public void Process(double dt) + { + this.ProcessPacket(new TickPacket(this.tick++)); + while (this.pendingPackets.Count > 0) + { + var packet = this.pendingPackets.Dequeue(); + this.publisher.Publish(packet); + } + this.sendGameState(); + } + + public void ProcessPacket(ValueType packet) + { + this.pendingPackets.Enqueue(packet); + } + + public void Init() + { + Task.Run(this.Run); + this.publisher = new Publisher(InteractorKind.Hybrid); + } + + public void Run() + { + this.server = new WatsonTcpServer("127.0.0.1", 42069); + this.server.Events.ClientConnected += this.clientConnected; + this.server.Events.ClientDisconnected += this.clientDisconnected; + this.server.Events.MessageReceived += this.messageReceived; + this.server.Start(); + } + + private void clientConnected(object sender, ConnectionEventArgs args) + { + Console.WriteLine("Client connected: " + args.IpPort); + var gameState = Context.Get().GameState; + var json = JsonConvert.SerializeObject(gameState); + if (sender is WatsonTcpServer server) + { + server.Send(args.IpPort, Encoding.UTF8.GetBytes(json)); + } + } + + private void clientDisconnected(object sender, DisconnectionEventArgs args) + { + Console.WriteLine("Client disconnected: " + args.IpPort); + } + + private void messageReceived(object sender, MessageReceivedEventArgs args) + { + var time = DateTime.Now; + Console.WriteLine("Message Received: " + args.IpPort); + var packet = Converter.ParsePacket(args.Data); + Console.WriteLine("Received packet: " + packet); + if (packet != null) + { + pendingPackets.Enqueue(packet); + } + Console.WriteLine(DateTime.Now - time); + } + + private void sendGameState() + { + if (server == null) + return; + var clients = server.ListClients(); + if (clients.Count() == 0) + return; + var gameState = Context.Get().GameState; + var json = JsonConvert.SerializeObject(gameState); + var bytes = Encoding.UTF8.GetBytes(json); + foreach (var client in clients) + { + server.Send(client, bytes); + } + } +} diff --git a/src/backend/IBackend.cs b/src/backend/IBackend.cs new file mode 100644 index 0000000..4533116 --- /dev/null +++ b/src/backend/IBackend.cs @@ -0,0 +1,6 @@ +interface IBackend +{ + public void Process(double dt); + public void ProcessPacket(ValueType packet); + public void Init(); +} diff --git a/src/backend/Publisher.cs b/src/backend/Publisher.cs new file mode 100644 index 0000000..4eb482c --- /dev/null +++ b/src/backend/Publisher.cs @@ -0,0 +1,86 @@ +class Publisher +{ + private readonly Dictionary> subscribers = + new(); + private readonly InteractorKind kind; + + public Publisher(InteractorKind kind) + { + this.kind = kind; + this.scan(); + } + + private void scan() + { + var assembly = this.GetType().Assembly; + var types = assembly.GetTypes(); + foreach (var type in types) + { + var attrs = type.GetCustomAttributes(typeof(Interactor), false); + if (attrs.Length == 0) + { + continue; + } + var methods = type.GetMethods(); + foreach (var method in methods) + { + var attrs2 = method.GetCustomAttributes(typeof(Interaction), false); + if (attrs2.Length == 0) + { + continue; + } + var attr = (Interaction)attrs2[0]; + if (attr.Kind != this.kind && this.kind != InteractorKind.Hybrid) + { + continue; + } + var del = Delegate.CreateDelegate( + typeof(Action<>).MakeGenericType(method.GetParameters()[0].ParameterType), + method + ); + this.subscribe(attr.Type, del); + } + } + } + + private void subscribe(string type, Delegate callback) + { + if (!this.subscribers.ContainsKey(type)) + { + this.subscribers[type] = new HashSet(); + } + this.subscribers[type].Add(callback); + } + + public void Dump() + { + foreach (var pair in this.subscribers) + { + Console.WriteLine(pair.Key); + foreach (var del in pair.Value) + { + Console.WriteLine(del); + } + } + } + + public void Publish(ValueType packet) + { + var type = PacketUtils.GetType(packet); + if (type != "tick") + { + Console.WriteLine("Publishing packet: " + type); + } + if (this.subscribers.ContainsKey(type)) + { + if (type != "tick") + { + Console.WriteLine("Found " + this.subscribers[type].Count + " subscribers"); + } + foreach (var del in this.subscribers[type]) + { + del.DynamicInvoke(packet); + } + } + } +} diff --git a/src/backend/RemoteBackend.cs b/src/backend/RemoteBackend.cs new file mode 100644 index 0000000..0584e22 --- /dev/null +++ b/src/backend/RemoteBackend.cs @@ -0,0 +1,52 @@ +using WatsonTcp; +using Newtonsoft.Json; +using System.Text; + +class RemoteBackend : IBackend +{ + private WatsonTcpClient client; + private Publisher publisher; + private Queue pendingPackets = new Queue(); + private uint tick = 0; + + public void Process(double dt) + { + var ctx = Context.Get(); + this.ProcessPacket(new TickPacket(tick++)); + while (pendingPackets.Count > 0) + { + var packet = pendingPackets.Dequeue(); + this.ProcessPacket(packet); + } + } + + public void ProcessPacket(ValueType packet) + { + this.publisher.Publish(packet); + var json = JsonConvert.SerializeObject(packet); + var bytes = Encoding.UTF8.GetBytes(json); + client.Send(bytes); + } + + public void Init() + { + Task.Run(this.Run); + this.publisher = new Publisher(InteractorKind.Client); + } + + public void Run() + { + client = new WatsonTcpClient("127.0.0.1", 42069); + client.Events.MessageReceived += (sender, args) => + { + var ctx = Context.Get(); + var message = Encoding.UTF8.GetString(args.Data); + var packet = JsonConvert.DeserializeObject(message); + if (packet != null) + { + ctx.GameState = packet; + } + }; + client.Connect(); + } +} diff --git a/src/backend/data/Packet.cs b/src/backend/data/Packet.cs new file mode 100644 index 0000000..3d30fc7 --- /dev/null +++ b/src/backend/data/Packet.cs @@ -0,0 +1,49 @@ +using System.Numerics; + +readonly struct MovePacket +{ + readonly public string type = "move"; + readonly public string playerName; + readonly public Vector2 movement; + + public MovePacket(string playerName, Vector2 movement) + { + this.playerName = playerName; + this.movement = movement; + } +} + +readonly struct ConnectPacket +{ + public readonly string type = "connect"; + public readonly string playerName; + public readonly Guid playerGuid; + + public ConnectPacket(string playerName, Guid playerGuid) + { + this.playerName = playerName; + this.playerGuid = playerGuid; + } +} + +readonly struct TickPacket +{ + public readonly string type = "tick"; + public readonly uint tick; + + public TickPacket(uint tick) + { + this.tick = tick; + } +} + +readonly struct SelfMovedPacket +{ + public readonly string type = "selfMoved"; + public readonly Vector2 target; + + public SelfMovedPacket(Vector2 target) + { + this.target = target; + } +} diff --git a/src/backend/interactor/Connect.cs b/src/backend/interactor/Connect.cs new file mode 100644 index 0000000..a222b52 --- /dev/null +++ b/src/backend/interactor/Connect.cs @@ -0,0 +1,22 @@ +[Interactor] +class Connect +{ + [Interaction(InteractorKind.Server, "connect")] + public static void ConnectServer(ConnectPacket packet) + { + var ctx = Context.Get(); + var player = ctx.GameState.Players.Find(p => p.Name == packet.playerName); + if (player == null) + { + ctx.GameState.Players.Add( + new Player + { + Name = packet.playerName, + Guid = packet.playerGuid, + Position = new Vector2(20, 16 * 16), + Movement = new Vector2(0, 0) + } + ); + } + } +} diff --git a/src/backend/interactor/Move.cs b/src/backend/interactor/Move.cs new file mode 100644 index 0000000..9327b0a --- /dev/null +++ b/src/backend/interactor/Move.cs @@ -0,0 +1,29 @@ +[Interactor] +class Move +{ + [Interaction(InteractorKind.Hybrid, "move")] + public static void MoveHybrid(MovePacket packet) + { + var ctx = Context.Get(); + var player = ctx.GameState.Players.Find(p => p.Name == packet.playerName); + if (player != null) + { + player.Movement = packet.movement * 4; + } + } + + [Interaction(InteractorKind.Hybrid, "tick")] + public static void TickHybrid(TickPacket packet) + { + var ctx = Context.Get(); + ctx.GameState.Players.ForEach(PlayerEntity.Move); + ctx.GameState.Players.ForEach(PlayerEntity.Collide); + } + + [Interaction(InteractorKind.Client, "tick")] + public static void SelfMovedClient(TickPacket packet) + { + var camera = Context.Get().FrontendGameState.Camera; + camera.CenterOn(PlayerEntity.GetSelf().Position); + } +} diff --git a/src/core/Bootstrapper.cs b/src/core/Bootstrapper.cs new file mode 100644 index 0000000..24023ed --- /dev/null +++ b/src/core/Bootstrapper.cs @@ -0,0 +1,12 @@ +class Bootstrapper +{ + public static void Bootstrap() + { + var ctx = Context.Get(); + ctx.GameState.World = new World(); + ctx.GameState.World.AddChunk(ChunkGenerator.CreateFilledChunk(0, 1, Tiles.stone)); + ctx.GameState.World.AddChunk(ChunkGenerator.CreateFilledChunk(-1, 1, Tiles.stone)); + ctx.GameState.World.AddChunk(ChunkGenerator.CreateFilledChunk(1, 1, Tiles.stone)); + ctx.GameState.World.AddChunk(ChunkGenerator.CreateFilledChunk(1, 0, Tiles.stone)); + } +} diff --git a/src/core/Camera.cs b/src/core/Camera.cs new file mode 100644 index 0000000..70be9e5 --- /dev/null +++ b/src/core/Camera.cs @@ -0,0 +1,19 @@ +class Camera +{ + public Vector2 position; + + public Camera() + { + position = Vector2.Zero; + } + + public void CenterOn(Vector2 target) + { + Console.WriteLine("Centering camera on " + target); + var ctx = Context.Get(); + var scale = ctx.FrontendGameState.Settings.GameScale; + var windowWidth = ctx.FrontendGameState.WindowWidth; + var windowHeight = ctx.FrontendGameState.WindowHeight; + position = target - (new Vector2(windowWidth / 2, windowHeight / 2)) / scale; + } +} diff --git a/src/core/Constants.cs b/src/core/Constants.cs new file mode 100644 index 0000000..df5de5e --- /dev/null +++ b/src/core/Constants.cs @@ -0,0 +1,6 @@ +class Constants +{ + public const int ChunkSize = 32; + public const int TileSize = 16; + public static Vector2 gravity = new Vector2(0, 0.1f); +} diff --git a/src/core/PlayerEntity.cs b/src/core/PlayerEntity.cs new file mode 100644 index 0000000..097a59e --- /dev/null +++ b/src/core/PlayerEntity.cs @@ -0,0 +1,69 @@ +class PlayerEntity +{ + public static bool isSelf(Player p) + { + return p.Guid == GetSelf().Guid; + } + + public static Player GetSelf() + { + var ctx = Context.Get(); + var player = ctx.GameState.Players.FirstOrDefault( + p => p.Guid == ctx.FrontendGameState.PlayerGuid + ); + return player; + } + + public static void Move(Player p) + { + p.Movement += Constants.gravity; + p.Position += p.Movement; + } + + public static void Collide(Player p) + { + var world = Context.Get().GameState.World; + bool hasCollision; + do + { + var pL = p.Position + new Vector2(0, -8); + hasCollision = + world.HasChunkAt(pL) && world.GetChunkAt(pL).hasTileAt(pL); + if (hasCollision) + { + p.Movement = p.Movement with { X = 0 }; + p.Position += new Vector2(0.1f, 0); + } + } while (hasCollision); + do + { + var pR = p.Position + new Vector2(16, -8); + hasCollision = + world.HasChunkAt(pR) && world.GetChunkAt(pR).hasTileAt(pR); + if (hasCollision) + { + p.Movement = p.Movement with { X = 0 }; + p.Position += new Vector2(-0.1f, 0); + } + } while (hasCollision); + do + { + var pL = p.Position; + var pR = p.Position + new Vector2(16, 0); + hasCollision = + world.HasChunkAt(pL) && world.GetChunkAt(pL).hasTileAt(pL) + || world.HasChunkAt(pR) && world.GetChunkAt(pR).hasTileAt(pR); + Console.WriteLine(World.ToChunkPos(p.Position)); + if (world.HasChunkAt(p.Position)) + { + var chunk = world.GetChunkAt(p.Position); + Console.WriteLine($"Chunk: {chunk.X}, {chunk.Y}"); + } + if (hasCollision) + { + p.Movement = p.Movement with { Y = 0 }; + p.Position += new Vector2(0, -0.1f); + } + } while (hasCollision); + } +} diff --git a/src/core/data/Chunk.cs b/src/core/data/Chunk.cs new file mode 100644 index 0000000..65d5722 --- /dev/null +++ b/src/core/data/Chunk.cs @@ -0,0 +1,63 @@ +class Chunk +{ + public int[,] Tiles { get; set; } = new int[Constants.ChunkSize, Constants.ChunkSize]; + public int X { get; set; } + public int Y { get; set; } + + public Chunk(int x, int y) + { + this.X = x; + this.Y = y; + } + + public void SetTile(int x, int y, int tile) + { + this.Tiles[x, y] = tile; + } + + public int GetTile(int x, int y) + { + return this.Tiles[x, y]; + } + + public bool hasTileAt(Vector2 pos) + { + return this.hasTileAt((int)pos.X, (int)pos.Y); + } + + public bool hasTileAt(int x, int y) + { + var posInChunk = this.GetPositionInChunk(new Vector2(x, y)); + var tileX = (int)Math.Floor(posInChunk.X / Constants.TileSize); + var tileY = (int)Math.Floor(posInChunk.Y / Constants.TileSize); + return this.hasTile(tileX, tileY); + } + + public int GetTileAt(Vector2 pos) + { + return this.GetTileAt((int)pos.X, (int)pos.Y); + } + + public int GetTileAt(int x, int y) + { + var tileX = (int)Math.Floor(x / (float)Constants.TileSize); + var tileY = (int)Math.Floor(y / (float)Constants.TileSize); + return this.GetTile(tileX, tileY); + } + + public bool hasTile(int x, int y) + { + return x >= 0 && x < this.Tiles.Length && y >= 0 && y < this.Tiles.Length; + } + + public bool hasTile(Vector2 pos) + { + return this.hasTile((int)pos.X, (int)pos.Y); + } + + public Vector2 GetPositionInChunk(Vector2 pos) + { + return pos - new Vector2(this.X * Constants.ChunkSize * Constants.TileSize, + this.Y * Constants.ChunkSize * Constants.TileSize); + } +} diff --git a/src/core/data/World.cs b/src/core/data/World.cs new file mode 100644 index 0000000..0465f38 --- /dev/null +++ b/src/core/data/World.cs @@ -0,0 +1,71 @@ +class World +{ + public Dictionary Chunks { get; set; } = new Dictionary(); + + public void AddChunk(Chunk chunk) + { + this.Chunks.Add(chunk.X + "," + chunk.Y, chunk); + } + + public Chunk GetChunk(Vector2 pos) + { + return this.GetChunk((int)pos.X, (int)pos.Y); + } + + public Chunk GetChunk(int x, int y) + { + return this.Chunks[x + "," + y]; + } + + public bool HasChunk(Vector2 pos) + { + return this.HasChunk((int)pos.X, (int)pos.Y); + } + + public bool HasChunk(int x, int y) + { + return this.Chunks.ContainsKey(x + "," + y); + } + + public Chunk GetChunkAt(Vector2 pos) + { + return this.GetChunkAt((int)pos.X, (int)pos.Y); + } + + public Chunk GetChunkAt(int x, int y) + { + var chunkPos = ToChunkPos(new Vector2(x, y)); + return this.Chunks[chunkPos.X + "," + chunkPos.Y]; + } + + public bool HasChunkAt(Vector2 pos) + { + return this.HasChunkAt((int)pos.X, (int)pos.Y); + } + + public bool HasChunkAt(int x, int y) + { + var chunkX = Math.Floor(x / (float)(Constants.ChunkSize * Constants.TileSize)); + var chunkY = Math.Floor(y / (float)(Constants.ChunkSize * Constants.TileSize)); + + return this.Chunks.ContainsKey(chunkX + "," + chunkY); + } + + public bool ChunkExists(int x, int y) + { + return this.Chunks.ContainsKey(x + "," + y); + } + + public static Vector2 ToChunkPos(Vector2 pos) + { + var chunkX = Math.Floor(pos.X / (Constants.ChunkSize * Constants.TileSize)); + var chunkY = Math.Floor(pos.Y / (Constants.ChunkSize * Constants.TileSize)); + return new Vector2((float)chunkX, (float)chunkY); + } + + public int GetTileAt(int x, int y) + { + var chunk = this.GetChunkAt(x, y); + return 0; + } +} diff --git a/src/core/tiles/Tile.cs b/src/core/tiles/Tile.cs new file mode 100644 index 0000000..6fed165 --- /dev/null +++ b/src/core/tiles/Tile.cs @@ -0,0 +1,48 @@ +class Tile +{ + public string Name { get; set; } + public IntPtr Texture { get; set; } + + public Tile(string name, string textureName) + { + this.Name = name; + + var rl = Context.Get().ResourceLoader; + var res = rl.LoadToIntPtr("assets." + textureName + ".png"); + var sdlBuffer = SDL_RWFromMem(res.ptr, res.size); + var surface = IMG_Load_RW(sdlBuffer, 1); + var texture = Context.Get().Renderer.CreateTextureFromSurface(surface); + this.Texture = texture; + SDL_FreeSurface(surface); + } + + ~Tile() + { + SDL_DestroyTexture(this.Texture); + } + + public void Render(int x, int y) + { + var renderer = Context.Get().Renderer; + var scale = Context.Get().FrontendGameState.Settings.GameScale; + var camera = Context.Get().FrontendGameState.Camera; + renderer.DrawTexture( + this.Texture, + (x - (int)camera.position.X) * scale, + (y - (int)camera.position.Y) * scale, + Constants.TileSize * scale, + Constants.TileSize * scale + ); + } + + public SDL_Rect GetCollisionRect(int x, int y) + { + return new SDL_Rect() + { + x = x, + y = y, + w = Constants.TileSize, + h = Constants.TileSize + }; + } +} diff --git a/src/core/tiles/TileRegistry.cs b/src/core/tiles/TileRegistry.cs new file mode 100644 index 0000000..61341ec --- /dev/null +++ b/src/core/tiles/TileRegistry.cs @@ -0,0 +1,19 @@ +enum Tiles : int +{ + stone = 1, +} + +class TileRegistry +{ + public Dictionary Tiles { get; set; } = new Dictionary(); + + public void RegisterTile() + { + Tiles.Add(1, new Tile("stone", "stone")); + } + + public Tile GetTile(int id) + { + return Tiles[id]; + } +} diff --git a/src/core/world/ChunkGenerator.cs b/src/core/world/ChunkGenerator.cs new file mode 100644 index 0000000..193253e --- /dev/null +++ b/src/core/world/ChunkGenerator.cs @@ -0,0 +1,20 @@ +class ChunkGenerator +{ + public static Chunk CreateFilledChunk(int x, int y, int fill) + { + var chunk = new Chunk(x, y); + for (var i = 0; i < Constants.ChunkSize; i++) + { + for (var j = 0; j < Constants.ChunkSize; j++) + { + chunk.SetTile(i, j, fill); + } + } + return chunk; + } + + public static Chunk CreateFilledChunk(int x, int y, Tiles fill) + { + return CreateFilledChunk(x, y, (int)fill); + } +} diff --git a/src/engine/AudioPlayer.cs b/src/engine/AudioPlayer.cs new file mode 100644 index 0000000..252191c --- /dev/null +++ b/src/engine/AudioPlayer.cs @@ -0,0 +1,25 @@ +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/FontManager.cs b/src/engine/FontManager.cs new file mode 100644 index 0000000..ec94791 --- /dev/null +++ b/src/engine/FontManager.cs @@ -0,0 +1,36 @@ +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..73963d5 --- /dev/null +++ b/src/engine/Game.cs @@ -0,0 +1,30 @@ +abstract class Game +{ + public const int TPS = 128; + 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(); + while (tAcc >= TimeSpan.FromSeconds(1.0 / TPS)) + { + update(dt.TotalSeconds); + tAcc -= TimeSpan.FromSeconds(1.0 / TPS); + } + + draw(); + } + } +} diff --git a/src/engine/PacketUtils.cs b/src/engine/PacketUtils.cs new file mode 100644 index 0000000..f5dc1bf --- /dev/null +++ b/src/engine/PacketUtils.cs @@ -0,0 +1,22 @@ +class PacketUtils +{ + public static string GetType(ValueType packet) + { + var t = packet.GetType(); + foreach (var pp in t.GetProperties()) + { + Console.WriteLine(pp.Name); + } + var p = t.GetField("type"); + if (p == null) + { + throw new Exception("p undef"); + } + var v = p.GetValue(packet); + if (v == null) + { + throw new Exception("v undef"); + } + return (string)v; + } +} diff --git a/src/engine/Renderer.cs b/src/engine/Renderer.cs new file mode 100644 index 0000000..fa46b10 --- /dev/null +++ b/src/engine/Renderer.cs @@ -0,0 +1,120 @@ +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); + } + + public void DrawTexture(IntPtr texture, int x, int y, int w, int h) + { + SDL_Rect rect = new() + { + x = x, + y = y, + w = w, + h = h + }; + SDL_RenderCopy(renderer, texture, IntPtr.Zero, ref rect); + } +} diff --git a/src/engine/ResourceLoader.cs b/src/engine/ResourceLoader.cs new file mode 100644 index 0000000..d17a3ad --- /dev/null +++ b/src/engine/ResourceLoader.cs @@ -0,0 +1,52 @@ +using System.Runtime.InteropServices; + +class ResourceLoader +{ + private string assemblyName; + + public ResourceLoader() + { + this.assemblyName = this.GetType().Assembly.GetName().Name!; + } + + public string LoadString(string resourceName) + { +#if (DEBUG) + Console.WriteLine("Loading resource: " + resourceName); + return File.ReadAllText(ToPath(resourceName)); +#endif + 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); + } + + public void SaveString(string resourceName, string content) + { + using var stream = new StreamWriter(ToPath(resourceName)); + stream.Write(content); + } + + private static string ToPath(string resourceName) + { + var s = resourceName.Split('.'); + return String.Join('/', s[..^1]) + "." + s[^1]; + } +} diff --git a/src/engine/Shapes.cs b/src/engine/Shapes.cs new file mode 100644 index 0000000..bffb4f4 --- /dev/null +++ b/src/engine/Shapes.cs @@ -0,0 +1,11 @@ +struct Line +{ + public Vector2 start; + public Vector2 end; + + public Line(Vector2 start, Vector2 end) + { + this.start = start; + this.end = end; + } +} diff --git a/src/engine/Window.cs b/src/engine/Window.cs new file mode 100644 index 0000000..720ec85 --- /dev/null +++ b/src/engine/Window.cs @@ -0,0 +1,44 @@ +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); + } + + public IntPtr GetRaw() + { + return this.window; + } +} diff --git a/src/engine/system/EventPriority.cs b/src/engine/system/EventPriority.cs new file mode 100644 index 0000000..3adb334 --- /dev/null +++ b/src/engine/system/EventPriority.cs @@ -0,0 +1,9 @@ +enum EventPriority : int +{ + Lowest = 0, + Low = 1, + Normal = 2, + High = 3, + Highest = 4, + Important = 5, +} diff --git a/src/engine/system/annotations/Interactor.cs b/src/engine/system/annotations/Interactor.cs new file mode 100644 index 0000000..0aec9f7 --- /dev/null +++ b/src/engine/system/annotations/Interactor.cs @@ -0,0 +1,22 @@ +enum InteractorKind +{ + Client, + Server, + Hybrid +} + +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] +class Interactor : Attribute { } + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +class Interaction : Attribute +{ + public string Type; + public InteractorKind Kind; + + public Interaction(InteractorKind kind, string type) + { + this.Type = type; + this.Kind = kind; + } +} diff --git a/src/engine/utils/Color.cs b/src/engine/utils/Color.cs new file mode 100644 index 0000000..ab3c5fb --- /dev/null +++ b/src/engine/utils/Color.cs @@ -0,0 +1,33 @@ +class Color +{ + public 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(); + 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/frontend/Frontend.cs b/src/frontend/Frontend.cs new file mode 100644 index 0000000..8997d03 --- /dev/null +++ b/src/frontend/Frontend.cs @@ -0,0 +1,141 @@ +class Frontend : IFrontend +{ + private string playerName = "Player"; + private bool fullscreen = false; + + public void Init() + { + var ctx = Context.Get(); + this.playerName = Context.Get().IsHost ? "Host" : "Client"; + var guid = Guid.NewGuid(); + ctx.FrontendGameState.PlayerGuid = guid; + var connectPacket = new ConnectPacket(playerName, guid); + ctx.Backend.ProcessPacket(connectPacket); + ctx.TileRegistry.RegisterTile(); + var (width, height) = ctx.Window.GetSize(); + ctx.FrontendGameState.WindowWidth = width; + ctx.FrontendGameState.WindowHeight = height; + } + + public void Process() + { + var ctx = Context.Get(); + while (SDL_PollEvent(out var e) != 0) + { + if (e.type == SDL_EventType.SDL_QUIT) + { + Environment.Exit(0); + } + if (e.type == SDL_EventType.SDL_WINDOWEVENT) + { + if (e.window.windowEvent == SDL_WindowEventID.SDL_WINDOWEVENT_RESIZED) + { + ctx.FrontendGameState.WindowWidth = e.window.data1; + ctx.FrontendGameState.WindowHeight = e.window.data2; + Console.WriteLine($"Window resized to {e.window.data1}x{e.window.data2}"); + var player = ctx.GameState.Players.Find( + p => p.Guid == ctx.FrontendGameState.PlayerGuid + ); + ctx.FrontendGameState.Camera.CenterOn(player.Position); + } + } + if (e.type == SDL_EventType.SDL_KEYDOWN && e.key.repeat == 0) + { + var movementInput = ctx.FrontendGameState.MovementInput; + if (e.key.keysym.scancode == SDL_Scancode.SDL_SCANCODE_F11) + { + if (!fullscreen) + { + SDL_SetWindowFullscreen( + ctx.Window.GetRaw(), + (uint)SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP + ); + } + else + { + SDL_SetWindowFullscreen(ctx.Window.GetRaw(), 0); + } + fullscreen = !fullscreen; + } + if (e.key.keysym.scancode == SDL_Scancode.SDL_SCANCODE_A) + { + movementInput.X -= 1; + } + if (e.key.keysym.scancode == SDL_Scancode.SDL_SCANCODE_D) + { + movementInput.X += 1; + } + if (e.key.keysym.scancode == SDL_Scancode.SDL_SCANCODE_W) + { + movementInput.Y -= 1; + } + if (e.key.keysym.scancode == SDL_Scancode.SDL_SCANCODE_S) + { + movementInput.Y += 1; + } + ctx.FrontendGameState.MovementInput = movementInput; + } + if (e.type == SDL_EventType.SDL_KEYUP && e.key.repeat == 0) + { + var movementInput = ctx.FrontendGameState.MovementInput; + if (e.key.keysym.scancode == SDL_Scancode.SDL_SCANCODE_A) + { + movementInput.X += 1; + } + if (e.key.keysym.scancode == SDL_Scancode.SDL_SCANCODE_D) + { + movementInput.X -= 1; + } + if (e.key.keysym.scancode == SDL_Scancode.SDL_SCANCODE_W) + { + movementInput.Y += 1; + } + if (e.key.keysym.scancode == SDL_Scancode.SDL_SCANCODE_S) + { + movementInput.Y -= 1; + } + ctx.FrontendGameState.MovementInput = movementInput; + } + if ( + e.key.keysym.scancode + is SDL_Scancode.SDL_SCANCODE_A + or SDL_Scancode.SDL_SCANCODE_D + or SDL_Scancode.SDL_SCANCODE_W + or SDL_Scancode.SDL_SCANCODE_S + && e.key.repeat == 0 + && e.type is SDL_EventType.SDL_KEYDOWN or SDL_EventType.SDL_KEYUP + ) + { + if (e.key.repeat == 1) + continue; + var movement = ctx.FrontendGameState.MovementInput; + if (movement.Length() > 0) + movement = Vector2.Normalize(movement); + ctx.Backend.ProcessPacket(new MovePacket(playerName, movement)); + } + if (e.key.keysym.scancode == SDL_Scancode.SDL_SCANCODE_ESCAPE) + { + Environment.Exit(0); + } + } + + ctx.Renderer.Clear(); + var scale = ctx.FrontendGameState.Settings.GameScale; + var camera = Context.Get().FrontendGameState.Camera; + new WorldRenderer().Render(); + ctx.GameState.Players.ForEach(player => + { + if (player.Name == playerName) + ctx.Renderer.SetColor(0, 0, 255, 255); + else + ctx.Renderer.SetColor(255, 0, 0, 255); + ctx.Renderer.DrawRect( + (player.Position.X - (int)camera.position.X) * scale, + (player.Position.Y - (int)camera.position.Y) * scale - 32 * scale, + 16 * scale, + 32 * scale + ); + }); + ctx.Renderer.Present(); + } +} diff --git a/src/frontend/IFrontend.cs b/src/frontend/IFrontend.cs new file mode 100644 index 0000000..06df2db --- /dev/null +++ b/src/frontend/IFrontend.cs @@ -0,0 +1,5 @@ +interface IFrontend +{ + public void Process(); + public void Init(); +} diff --git a/src/frontend/renderer/IRenderer.cs b/src/frontend/renderer/IRenderer.cs new file mode 100644 index 0000000..7423328 --- /dev/null +++ b/src/frontend/renderer/IRenderer.cs @@ -0,0 +1,4 @@ +interface IRenderer +{ + public void Render(); +} diff --git a/src/frontend/renderer/WorldRenderer.cs b/src/frontend/renderer/WorldRenderer.cs new file mode 100644 index 0000000..f80c9d8 --- /dev/null +++ b/src/frontend/renderer/WorldRenderer.cs @@ -0,0 +1,24 @@ +class WorldRenderer : IRenderer +{ + public void Render() + { + var ctx = Context.Get(); + var world = ctx.GameState.World; + var renderer = ctx.Renderer; + var tileRegistry = ctx.TileRegistry; + foreach (var (_, chunk) in world.Chunks) + { + for (int y = 0; y < Constants.ChunkSize; y++) + { + for (int x = 0; x < Constants.ChunkSize; x++) + { + var tileId = chunk.GetTile(x, y); + var tile = tileRegistry.GetTile(tileId); + var chunkOffsetX = chunk.X * Constants.TileSize * Constants.ChunkSize; + var chunkOffsetY = chunk.Y * Constants.TileSize * Constants.ChunkSize; + tile.Render(x * 16 + chunkOffsetX, y * 16 + chunkOffsetY); + } + } + } + } +} diff --git a/src/network/Converter.cs b/src/network/Converter.cs new file mode 100644 index 0000000..2aa6a1f --- /dev/null +++ b/src/network/Converter.cs @@ -0,0 +1,36 @@ +using System.Text; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json; + +class Converter +{ + public static ValueType ParsePacket(byte[] bytes) + { + var jsonString = Encoding.UTF8.GetString(bytes); + return ParsePacket(jsonString); + } + + public static ValueType ParsePacket(string jsonString) + { + var parsedRaw = JObject.Parse(jsonString); + var type = parsedRaw.GetValue("type"); + if (type == null) + { + throw new Exception("Packet has no type"); + } + var packetType = type.Value(); + Console.WriteLine("Packet type: " + packetType); + return packetType switch + { + "move" => parsedRaw.ToObject(), + "connect" => parsedRaw.ToObject(), + _ => throw new Exception("Unknown packet type") + }; + } + + public static byte[] SerializePacket(ValueType packet) + { + var jsonString = JsonConvert.SerializeObject(packet); + return Encoding.UTF8.GetBytes(jsonString); + } +} diff --git a/src/state/GameState.cs b/src/state/GameState.cs new file mode 100644 index 0000000..de86c15 --- /dev/null +++ b/src/state/GameState.cs @@ -0,0 +1,23 @@ +class FrontendGameState +{ + public Vector2 MovementInput; + public Vector2 CameraPosition; + public int WindowWidth; + public int WindowHeight; + public Guid PlayerGuid; + public Camera Camera = new Camera(); + public Settings Settings { get; set; } = new Settings(); +} + +class Settings +{ + public int GameScale = 4; + public int UIScale = 4; + public bool ShowCollision = true; +} + +class GameState +{ + public List Players { get; set; } = new List(); + public World World { get; set; } +} diff --git a/src/state/Player.cs b/src/state/Player.cs new file mode 100644 index 0000000..8a40db7 --- /dev/null +++ b/src/state/Player.cs @@ -0,0 +1,12 @@ +class Player +{ + public string Name; + public Vector2 Position; + public Vector2 Movement; + public Guid Guid; + + public Line GetBottomCollisionLine() + { + return new Line(Position, Position + new Vector2(16, 0)); + } +}