first commit

This commit is contained in:
MasterGordon 2022-11-05 12:59:45 +01:00
commit 69596ab172
47 changed files with 1588 additions and 0 deletions

71
.editorconfig Normal file
View File

@ -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

32
.github/workflows/dotnet.yml vendored Normal file
View File

@ -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

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
bin/
obj/

13
Program.cs Normal file
View File

@ -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!");
}
}

BIN
assets/font.ttf Normal file

Binary file not shown.

BIN
assets/stone.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 B

24
mine2d.csproj Normal file
View File

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>mine2d</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<PublishTrimmed>true</PublishTrimmed>
<TrimmerDefaultAction>link</TrimmerDefaultAction>
<PublishReadyToRun>true</PublishReadyToRun>
<PublishSingleFile>true</PublishSingleFile>
<Nullable>disable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.SDL2-CS" Version="1.0.596-alpha" />
<PackageReference Include="WatsonTcp" Version="4.8.14.14" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="assets/*" />
</ItemGroup>
</Project>

2
readme.md Normal file
View File

@ -0,0 +1,2 @@
# Boulder Dash SDL

45
src/Context.cs Normal file
View File

@ -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;
}
}

35
src/Controls.cs Normal file
View File

@ -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");
}
}
}

4
src/Import.cs Normal file
View File

@ -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;

46
src/Mine2d.cs Normal file
View File

@ -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);
}
}

87
src/backend/Backend.cs Normal file
View File

@ -0,0 +1,87 @@
using System.Text;
using Newtonsoft.Json;
using WatsonTcp;
class Backend : IBackend
{
private WatsonTcpServer server;
private Publisher publisher;
private Queue<ValueType> 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);
}
}
}

6
src/backend/IBackend.cs Normal file
View File

@ -0,0 +1,6 @@
interface IBackend
{
public void Process(double dt);
public void ProcessPacket(ValueType packet);
public void Init();
}

86
src/backend/Publisher.cs Normal file
View File

@ -0,0 +1,86 @@
class Publisher
{
private readonly Dictionary<string, HashSet<Delegate>> 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<Delegate>();
}
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);
}
}
}
}

View File

@ -0,0 +1,52 @@
using WatsonTcp;
using Newtonsoft.Json;
using System.Text;
class RemoteBackend : IBackend
{
private WatsonTcpClient client;
private Publisher publisher;
private Queue<ValueType> pendingPackets = new Queue<ValueType>();
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<GameState>(message);
if (packet != null)
{
ctx.GameState = packet;
}
};
client.Connect();
}
}

View File

@ -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;
}
}

View File

@ -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)
}
);
}
}
}

View File

@ -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);
}
}

12
src/core/Bootstrapper.cs Normal file
View File

@ -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));
}
}

19
src/core/Camera.cs Normal file
View File

@ -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;
}
}

6
src/core/Constants.cs Normal file
View File

@ -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);
}

69
src/core/PlayerEntity.cs Normal file
View File

@ -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);
}
}

63
src/core/data/Chunk.cs Normal file
View File

@ -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);
}
}

71
src/core/data/World.cs Normal file
View File

@ -0,0 +1,71 @@
class World
{
public Dictionary<string, Chunk> Chunks { get; set; } = new Dictionary<string, Chunk>();
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;
}
}

48
src/core/tiles/Tile.cs Normal file
View File

@ -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
};
}
}

View File

@ -0,0 +1,19 @@
enum Tiles : int
{
stone = 1,
}
class TileRegistry
{
public Dictionary<int, Tile> Tiles { get; set; } = new Dictionary<int, Tile>();
public void RegisterTile()
{
Tiles.Add(1, new Tile("stone", "stone"));
}
public Tile GetTile(int id)
{
return Tiles[id];
}
}

View File

@ -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);
}
}

25
src/engine/AudioPlayer.cs Normal file
View File

@ -0,0 +1,25 @@
enum Sound { }
class AudioPlayer
{
private Dictionary<Sound, byte[]> 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);
}
}

36
src/engine/FontManager.cs Normal file
View File

@ -0,0 +1,36 @@
using static SDL2.SDL_ttf;
using static SDL2.SDL;
class FontManager
{
private Dictionary<string, IntPtr> 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];
}
}

30
src/engine/Game.cs Normal file
View File

@ -0,0 +1,30 @@
abstract class Game
{
public const int TPS = 128;
private Queue<int> fpsQueue = new Queue<int>();
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();
}
}
}

22
src/engine/PacketUtils.cs Normal file
View File

@ -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;
}
}

120
src/engine/Renderer.cs Normal file
View File

@ -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);
}
}

View File

@ -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];
}
}

11
src/engine/Shapes.cs Normal file
View File

@ -0,0 +1,11 @@
struct Line
{
public Vector2 start;
public Vector2 end;
public Line(Vector2 start, Vector2 end)
{
this.start = start;
this.end = end;
}
}

44
src/engine/Window.cs Normal file
View File

@ -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;
}
}

View File

@ -0,0 +1,9 @@
enum EventPriority : int
{
Lowest = 0,
Low = 1,
Normal = 2,
High = 3,
Highest = 4,
Important = 5,
}

View File

@ -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;
}
}

33
src/engine/utils/Color.cs Normal file
View File

@ -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;
}
}

View File

@ -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));
}
}

141
src/frontend/Frontend.cs Normal file
View File

@ -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();
}
}

View File

@ -0,0 +1,5 @@
interface IFrontend
{
public void Process();
public void Init();
}

View File

@ -0,0 +1,4 @@
interface IRenderer
{
public void Render();
}

View File

@ -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);
}
}
}
}
}

36
src/network/Converter.cs Normal file
View File

@ -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<string>();
Console.WriteLine("Packet type: " + packetType);
return packetType switch
{
"move" => parsedRaw.ToObject<MovePacket>(),
"connect" => parsedRaw.ToObject<ConnectPacket>(),
_ => throw new Exception("Unknown packet type")
};
}
public static byte[] SerializePacket(ValueType packet)
{
var jsonString = JsonConvert.SerializeObject(packet);
return Encoding.UTF8.GetBytes(jsonString);
}
}

23
src/state/GameState.cs Normal file
View File

@ -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<Player> Players { get; set; } = new List<Player>();
public World World { get; set; }
}

12
src/state/Player.cs Normal file
View File

@ -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));
}
}