Add chunks and track them

Only chunks that are close to players
are sent to them, as well as actions
occuring in those chunks.

- Add Chunk and ChunkLayer classes / nodes
  In the future, chunks could make use of multiple
  layers such as terrain, building, interior, liquids,
  pipes and wires, ...
- Add PlayerVisibilityTracker
  Used on the server to keep track of which
  chunks a player can see, firing events when
  chunks go in or out of range.
- Add RPC extension methods which accept
  an IEnumerable<int> for players to send to
- Add GetPlayersTracking extension method
- Use GlobalPosition instead of Position where needed
- Update WorldSave to save Chunks
  (Incremented WorldSave.LATEST_VERSION)
- Add methods to BlockPos relating to chunk position
- Improve upon Facing with BlockPos.Add/Subtract
- Add generic GetChildren extension method
- Add GetOrCreateChild extension method
main
copygirl 4 years ago
parent 4aeb380aa8
commit 7c4ae2ce45
  1. 8
      scene/GameScene.tscn
  2. 53
      scene/HitDecal.tscn
  3. 72
      src/Chunk.cs
  4. 2
      src/EscapeMenuMultiplayer.cs
  5. 5
      src/EscapeMenuWorld.cs
  6. 67
      src/IO/WorldSave.cs
  7. 14
      src/Items/CreativeBuilding.cs
  8. 2
      src/Items/Items.cs
  9. 9
      src/Items/Weapon.cs
  10. 52
      src/Network/PlayerVisibilityTracker.cs
  11. 7
      src/Objects/Block.cs
  12. 5
      src/Objects/Bullet.cs
  13. 9
      src/Objects/Player.cs
  14. 2
      src/Scenes/Client.cs
  15. 5
      src/Scenes/Server.cs
  16. 24
      src/Utility/BlockPos.cs
  17. 18
      src/Utility/Extensions.cs
  18. 34
      src/Utility/Facing.cs
  19. 28
      src/Utility/RPC.cs
  20. 83
      src/World.cs

@ -9,9 +9,5 @@ script = ExtResource( 3 )
[node name="World" type="Node" parent="."] [node name="World" type="Node" parent="."]
script = ExtResource( 1 ) script = ExtResource( 1 )
PlayerContainerPath = NodePath("Players") PlayerContainerPath = NodePath("")
BlockContainerPath = NodePath("Blocks") BlockContainerPath = NodePath("")
[node name="Players" type="Node" parent="World"]
[node name="Blocks" type="Node" parent="World"]

@ -3,16 +3,16 @@
[ext_resource path="res://gfx/hit_decal.png" type="Texture" id=1] [ext_resource path="res://gfx/hit_decal.png" type="Texture" id=1]
[ext_resource path="res://src/Objects/HitDecal.cs" type="Script" id=2] [ext_resource path="res://src/Objects/HitDecal.cs" type="Script" id=2]
[sub_resource type="VisualShaderNodeInput" id=15] [sub_resource type="VisualShaderNodeInput" id=1]
input_name = "modulate_color" input_name = "modulate_color"
[sub_resource type="VisualShaderNodeInput" id=16] [sub_resource type="VisualShaderNodeInput" id=2]
input_name = "modulate_alpha" input_name = "modulate_alpha"
[sub_resource type="VisualShaderNodeScalarOp" id=17] [sub_resource type="VisualShaderNodeScalarOp" id=3]
operator = 2 operator = 2
[sub_resource type="VisualShaderNodeExpression" id=18] [sub_resource type="VisualShaderNodeExpression" id=4]
size = Vector2( 512, 260 ) size = Vector2( 512, 260 )
expression = "vec2 tex_size = vec2(textureSize(mask, 0)); expression = "vec2 tex_size = vec2(textureSize(mask, 0));
vec2 pix_loc = uv.xy / TEXTURE_PIXEL_SIZE; vec2 pix_loc = uv.xy / TEXTURE_PIXEL_SIZE;
@ -20,30 +20,30 @@ mask_uv = vec3((pix_loc + offset.xy) / tex_size, 0);
outside = mask_uv.x >= 0.0 && mask_uv.x <= 1.0 && outside = mask_uv.x >= 0.0 && mask_uv.x <= 1.0 &&
mask_uv.y >= 0.0 && mask_uv.y <= 1.0;" mask_uv.y >= 0.0 && mask_uv.y <= 1.0;"
[sub_resource type="VisualShaderNodeInput" id=19] [sub_resource type="VisualShaderNodeInput" id=5]
input_name = "uv" input_name = "uv"
[sub_resource type="VisualShaderNodeScalarSwitch" id=20] [sub_resource type="VisualShaderNodeScalarSwitch" id=6]
[sub_resource type="VisualShaderNodeVectorOp" id=21] [sub_resource type="VisualShaderNodeVectorOp" id=7]
operator = 2 operator = 2
[sub_resource type="VisualShaderNodeTexture" id=22] [sub_resource type="VisualShaderNodeTexture" id=8]
source = 5 source = 5
[sub_resource type="VisualShaderNodeInput" id=23] [sub_resource type="VisualShaderNodeInput" id=9]
input_name = "texture" input_name = "texture"
[sub_resource type="VisualShaderNodeScalarOp" id=24] [sub_resource type="VisualShaderNodeScalarOp" id=10]
operator = 2 operator = 2
[sub_resource type="VisualShaderNodeVec3Uniform" id=25] [sub_resource type="VisualShaderNodeVec3Uniform" id=11]
uniform_name = "offset" uniform_name = "offset"
[sub_resource type="VisualShaderNodeTextureUniform" id=26] [sub_resource type="VisualShaderNodeTextureUniform" id=12]
uniform_name = "mask" uniform_name = "mask"
[sub_resource type="VisualShader" id=27] [sub_resource type="VisualShader" id=13]
code = "shader_type canvas_item; code = "shader_type canvas_item;
uniform vec3 offset; uniform vec3 offset;
uniform sampler2D mask; uniform sampler2D mask;
@ -133,27 +133,26 @@ void light() {
} }
" "
graph_offset = Vector2( 341, -275 )
mode = 1 mode = 1
flags/light_only = false flags/light_only = false
nodes/fragment/0/position = Vector2( 840, -100 ) nodes/fragment/0/position = Vector2( 840, -100 )
nodes/fragment/2/node = SubResource( 22 ) nodes/fragment/2/node = SubResource( 8 )
nodes/fragment/2/position = Vector2( 400, -40 ) nodes/fragment/2/position = Vector2( 400, -40 )
nodes/fragment/3/node = SubResource( 23 ) nodes/fragment/3/node = SubResource( 9 )
nodes/fragment/3/position = Vector2( 220, -40 ) nodes/fragment/3/position = Vector2( 220, -40 )
nodes/fragment/6/node = SubResource( 24 ) nodes/fragment/6/node = SubResource( 10 )
nodes/fragment/6/position = Vector2( 640, 60 ) nodes/fragment/6/position = Vector2( 640, 60 )
nodes/fragment/7/node = SubResource( 25 ) nodes/fragment/7/node = SubResource( 11 )
nodes/fragment/7/position = Vector2( -340, 60 ) nodes/fragment/7/position = Vector2( -340, 60 )
nodes/fragment/8/node = SubResource( 26 ) nodes/fragment/8/node = SubResource( 12 )
nodes/fragment/8/position = Vector2( 400, 100 ) nodes/fragment/8/position = Vector2( 400, 100 )
nodes/fragment/10/node = SubResource( 15 ) nodes/fragment/10/node = SubResource( 1 )
nodes/fragment/10/position = Vector2( 320, -200 ) nodes/fragment/10/position = Vector2( 320, -200 )
nodes/fragment/13/node = SubResource( 16 ) nodes/fragment/13/node = SubResource( 2 )
nodes/fragment/13/position = Vector2( 320, -120 ) nodes/fragment/13/position = Vector2( 320, -120 )
nodes/fragment/14/node = SubResource( 17 ) nodes/fragment/14/node = SubResource( 3 )
nodes/fragment/14/position = Vector2( 640, -60 ) nodes/fragment/14/position = Vector2( 640, -60 )
nodes/fragment/15/node = SubResource( 18 ) nodes/fragment/15/node = SubResource( 4 )
nodes/fragment/15/position = Vector2( -140, 40 ) nodes/fragment/15/position = Vector2( -140, 40 )
nodes/fragment/15/size = Vector2( 512, 260 ) nodes/fragment/15/size = Vector2( 512, 260 )
nodes/fragment/15/input_ports = "0,1,offset;1,1,uv;" nodes/fragment/15/input_ports = "0,1,offset;1,1,uv;"
@ -163,16 +162,16 @@ vec2 pix_loc = uv.xy / TEXTURE_PIXEL_SIZE;
mask_uv = vec3((pix_loc + offset.xy) / tex_size, 0); mask_uv = vec3((pix_loc + offset.xy) / tex_size, 0);
outside = mask_uv.x >= 0.0 && mask_uv.x <= 1.0 && outside = mask_uv.x >= 0.0 && mask_uv.x <= 1.0 &&
mask_uv.y >= 0.0 && mask_uv.y <= 1.0;" mask_uv.y >= 0.0 && mask_uv.y <= 1.0;"
nodes/fragment/16/node = SubResource( 19 ) nodes/fragment/16/node = SubResource( 5 )
nodes/fragment/16/position = Vector2( -340, 140 ) nodes/fragment/16/position = Vector2( -340, 140 )
nodes/fragment/17/node = SubResource( 20 ) nodes/fragment/17/node = SubResource( 6 )
nodes/fragment/17/position = Vector2( 620, 180 ) nodes/fragment/17/position = Vector2( 620, 180 )
nodes/fragment/18/node = SubResource( 21 ) nodes/fragment/18/node = SubResource( 7 )
nodes/fragment/18/position = Vector2( 640, -180 ) nodes/fragment/18/position = Vector2( 640, -180 )
nodes/fragment/connections = PoolIntArray( 3, 0, 2, 2, 2, 1, 6, 0, 6, 0, 14, 1, 13, 0, 14, 0, 14, 0, 0, 1, 7, 0, 15, 0, 16, 0, 15, 1, 15, 0, 8, 0, 15, 1, 17, 0, 8, 1, 17, 1, 17, 0, 6, 1, 10, 0, 18, 0, 2, 0, 18, 1, 18, 0, 0, 0 ) nodes/fragment/connections = PoolIntArray( 3, 0, 2, 2, 2, 1, 6, 0, 6, 0, 14, 1, 13, 0, 14, 0, 14, 0, 0, 1, 7, 0, 15, 0, 16, 0, 15, 1, 15, 0, 8, 0, 15, 1, 17, 0, 8, 1, 17, 1, 17, 0, 6, 1, 10, 0, 18, 0, 2, 0, 18, 1, 18, 0, 0, 0 )
[sub_resource type="ShaderMaterial" id=14] [sub_resource type="ShaderMaterial" id=14]
shader = SubResource( 27 ) shader = SubResource( 13 )
shader_param/offset = Vector3( 0, 0, 0 ) shader_param/offset = Vector3( 0, 0, 0 )
[node name="HitDecal" type="Sprite"] [node name="HitDecal" type="Sprite"]

@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using Godot;
public class Chunk : Node2D
{
public const int LENGTH = 32;
public const int BIT_SHIFT = 5;
public const int BIT_MASK = ~(~0 << BIT_SHIFT);
public (int, int) ChunkPosition { get; }
public Chunk(int x, int y)
{
ChunkPosition = (x, y);
Position = new Vector2(x << (BIT_SHIFT + Block.BIT_SHIFT), y << (BIT_SHIFT + Block.BIT_SHIFT));
}
public ChunkLayer<T> GetLayerOrNull<T>()
=> GetNodeOrNull<ChunkLayer<T>>($"{typeof(T).Name}Layer");
public ChunkLayer<T> GetOrCreateLayer<T>()
{
var layer = GetLayerOrNull<T>();
if (layer == null) AddChild(layer = new ChunkLayer<T> { Name = $"{typeof(T).Name}Layer" });
return layer;
}
// TODO: How should we handle chunk extends? Blocks can go "outside" of the current extends, since they're centered.
// public override void _Draw()
// => DrawRect(new Rect2(Vector2.Zero, Vector2.One * (LENGTH * Block.LENGTH)), Colors.Blue, false);
}
public class ChunkLayer<T> : Node2D
{
private static readonly IEqualityComparer<T> COMPARER = EqualityComparer<T>.Default;
// TODO: Use one-dimensional array?
private readonly T[,] _data = new T[Chunk.LENGTH, Chunk.LENGTH];
private int _numNonDefault = 0;
public T this[BlockPos pos] {
get => this[pos.X, pos.Y];
set => this[pos.X, pos.Y] = value;
}
public T this[int x, int y] {
get { EnsureWithin(x, y); return _data[x, y]; }
set {
EnsureWithin(x, y);
var previous = _data[x, y];
if (COMPARER.Equals(value, previous)) return;
if (!COMPARER.Equals(previous, default)) {
if (previous is Node node) RemoveChild(node);
_numNonDefault--;
}
if (!COMPARER.Equals(value, default)) {
if (value is Node node) AddChild(node);
_numNonDefault++;
}
_data[x, y] = value;
}
}
public bool IsDefault => _numNonDefault == 0;
private static void EnsureWithin(int x, int y)
{
if ((x < 0) || (x >= Chunk.LENGTH) || (y < 0) || (y >= Chunk.LENGTH)) throw new ArgumentException(
$"x and y ({x},{y}) must be within chunk boundaries - (0,0) inclusive to ({Chunk.LENGTH},{Chunk.LENGTH}) exclusive");
}
}

@ -123,7 +123,7 @@ public class EscapeMenuMultiplayer : Container
if (server.IsRunning) { if (server.IsRunning) {
server.Stop(); server.Stop();
server.GetWorld().ClearPlayers(); server.GetWorld().ClearPlayers();
server.GetWorld().ClearBlocks(); server.GetWorld().ClearChunks();
client.Disconnect(); client.Disconnect();
} }

@ -103,10 +103,13 @@ public class EscapeMenuWorld : CenterContainer
var server = this.GetClient().GetNode<IntegratedServer>(nameof(IntegratedServer)).Server; var server = this.GetClient().GetNode<IntegratedServer>(nameof(IntegratedServer)).Server;
var save = WorldSave.ReadFromFile(path); var save = WorldSave.ReadFromFile(path);
foreach (var player in server.GetWorld().Players) {
// Reset players' positions. // Reset players' positions.
foreach (var player in server.GetWorld().Players)
// Can't use RPC helper method here since player is not a LocalPlayer here. // Can't use RPC helper method here since player is not a LocalPlayer here.
player.RpcId(player.NetworkID, nameof(LocalPlayer.ResetPosition), Vector2.Zero); player.RpcId(player.NetworkID, nameof(LocalPlayer.ResetPosition), Vector2.Zero);
// Reset the visbility tracker so the client will receive new chunks.
player.VisibilityTracker.Reset();
}
save.ReadDataIntoWorld(server.GetWorld()); save.ReadDataIntoWorld(server.GetWorld());
_playtime = save.Playtime; _playtime = save.Playtime;

@ -9,7 +9,7 @@ public class WorldSave
{ {
public const string FILE_EXT = ".yf5"; public const string FILE_EXT = ".yf5";
public const int MAGIC_NUMBER = 0x59463573; // "YF5s" public const int MAGIC_NUMBER = 0x59463573; // "YF5s"
public const int LATEST_VERSION = 0; public const int LATEST_VERSION = 1;
public static readonly string WORLDS_DIR = OS.GetUserDataDir() + "/worlds/"; public static readonly string WORLDS_DIR = OS.GetUserDataDir() + "/worlds/";
@ -18,7 +18,7 @@ public class WorldSave
public int Version { get; private set; } = LATEST_VERSION; public int Version { get; private set; } = LATEST_VERSION;
public TimeSpan Playtime { get; set; } = TimeSpan.Zero; public TimeSpan Playtime { get; set; } = TimeSpan.Zero;
public List<(BlockPos, Color, bool)> Blocks { get; private set; } public Dictionary<(int, int), Dictionary<BlockPos, (Color, bool)>> Chunks { get; private set; }
public static WorldSave ReadFromFile(string path) public static WorldSave ReadFromFile(string path)
@ -30,19 +30,34 @@ public class WorldSave
if (magic != MAGIC_NUMBER) throw new IOException( if (magic != MAGIC_NUMBER) throw new IOException(
$"Magic number does not match ({magic:X8} != {MAGIC_NUMBER:X8})"); $"Magic number does not match ({magic:X8} != {MAGIC_NUMBER:X8})");
// TODO: See how to support multiple versions. // TODO: See how to better support multiple versions, improve saving/loading.
save.Version = reader.ReadUInt16(); save.Version = reader.ReadUInt16();
if (save.Version != LATEST_VERSION) throw new IOException(
$"Version does not match ({save.Version} != {LATEST_VERSION})");
save.Playtime = TimeSpan.FromSeconds(reader.ReadUInt32()); save.Playtime = TimeSpan.FromSeconds(reader.ReadUInt32());
if (save.Version == 0) {
save.Chunks = new Dictionary<(int, int), Dictionary<BlockPos, (Color, bool)>>();
var numBlocks = reader.ReadInt32(); var numBlocks = reader.ReadInt32();
save.Blocks = new List<(BlockPos, Color, bool)>(); for (var i = 0; i < numBlocks; i++) {
for (var i = 0; i < numBlocks; i++) var blockPos = new BlockPos(reader.ReadInt32(), reader.ReadInt32());
save.Blocks.Add((new BlockPos(reader.ReadInt32(), reader.ReadInt32()), var blockData = (new Color(reader.ReadInt32()), reader.ReadBoolean());
new Color(reader.ReadInt32()), var chunkPos = blockPos.ToChunkPos();
reader.ReadBoolean())); if (!save.Chunks.TryGetValue(chunkPos, out var blocks))
save.Chunks.Add(chunkPos, blocks = new Dictionary<BlockPos, (Color, bool)>());
blocks.Add(blockPos.GlobalToChunkRel(), blockData);
}
} else if (save.Version == 1) {
var numChunks = reader.ReadInt32();
save.Chunks = new Dictionary<(int, int), Dictionary<BlockPos, (Color, bool)>>(numChunks);
for (var i = 0; i < numChunks; i++) {
var chunkPos = (reader.ReadInt32(), reader.ReadInt32());
var numBlocks = (int)reader.ReadUInt16();
var blocks = new Dictionary<BlockPos, (Color, bool)>(numBlocks);
for (var j = 0; j < numBlocks; j++)
blocks.Add(new BlockPos(reader.ReadByte(), reader.ReadByte()),
(new Color(reader.ReadInt32()), reader.ReadBoolean()));
save.Chunks.Add(chunkPos, blocks);
}
} else throw new IOException($"Version {save.Version} not supported (latest version: {LATEST_VERSION})");
} }
} }
return save; return save;
@ -56,27 +71,41 @@ public class WorldSave
writer.Write((ushort)LATEST_VERSION); writer.Write((ushort)LATEST_VERSION);
writer.Write((uint)Playtime.TotalSeconds); writer.Write((uint)Playtime.TotalSeconds);
writer.Write(Blocks.Count); writer.Write(Chunks.Count);
foreach (var (position, color, unbreakable) in Blocks) { foreach (var ((chunkX, chunkY), blocks) in Chunks) {
writer.Write(position.X); writer.Write(chunkX);
writer.Write(position.Y); writer.Write(chunkY);
writer.Write((ushort)blocks.Count);
foreach (var ((blockX, blockY), (color, unbreakable)) in blocks) {
writer.Write((byte)blockX);
writer.Write((byte)blockY);
writer.Write(color.ToRgba32()); writer.Write(color.ToRgba32());
writer.Write(unbreakable); writer.Write(unbreakable);
} }
} }
} }
}
new Godot.Directory().Rename(path + ".tmp", path); new Godot.Directory().Rename(path + ".tmp", path);
LastSaved = File.GetLastWriteTime(path); LastSaved = File.GetLastWriteTime(path);
} }
public void WriteDataFromWorld(World world) public void WriteDataFromWorld(World world)
=> Blocks = world.Blocks.Select(block => (block.Position, block.Color, block.Unbreakable)).ToList(); => Chunks = world.Chunks.ToDictionary(
chunk => chunk.ChunkPosition,
chunk => chunk.GetLayerOrNull<Block>()
.GetChildren<Block>().ToDictionary(
block => block.ChunkLocalBlockPos,
block => (block.Color, block.Unbreakable)));
public void ReadDataIntoWorld(World world) public void ReadDataIntoWorld(World world)
{ {
RPC.Reliable(world.ClearBlocks); RPC.Reliable(world.ClearChunks);
foreach (var (position, color, unbreakable) in Blocks) foreach (var (chunkPos, blocks) in Chunks) {
RPC.Reliable(world.SpawnBlock, position.X, position.Y, color, unbreakable); foreach (var (blockPos, (color, unbreakable)) in blocks) {
var (x, y) = blockPos.ChunkRelToGlobal(chunkPos);
world.SpawnBlock(x, y, color, unbreakable);
}
}
} }
} }

@ -39,14 +39,12 @@ public class CreativeBuilding : Node2D
if (ev.IsActionPressed("interact_primary")) { if (ev.IsActionPressed("interact_primary")) {
GetTree().SetInputAsHandled(); GetTree().SetInputAsHandled();
_currentMode = (((_currentMode == null) && _canBuild) ? BuildMode.Placing : (BuildMode?)null); _currentMode = ((_currentMode == null) && _canBuild) ? BuildMode.Placing : (BuildMode?)null;
} }
if (ev.IsActionPressed("interact_secondary")) { if (ev.IsActionPressed("interact_secondary")) {
GetTree().SetInputAsHandled(); GetTree().SetInputAsHandled();
_currentMode = ((_currentMode == null) ? BuildMode.Breaking : (BuildMode?)null); _currentMode = (_currentMode == null) ? BuildMode.Breaking : (BuildMode?)null;
} }
// NOTE: These ternary operations require extra brackets for some
// reason or else the syntax highlighting in VS Code breaks?!
} }
public override void _Process(float delta) public override void _Process(float delta)
@ -106,7 +104,7 @@ public class CreativeBuilding : Node2D
} }
private static IEnumerable<BlockPos> GetBlockPositions(BlockPos start, Facing direction, int length) private static IEnumerable<BlockPos> GetBlockPositions(BlockPos start, Facing direction, int length)
=> Enumerable.Range(0, length + 1).Select(i => start + direction.ToBlockPos() * i); => Enumerable.Range(0, length + 1).Select(i => start.Add(direction, i));
[Master] [Master]
@ -127,7 +125,8 @@ public class CreativeBuilding : Node2D
foreach (var pos in GetBlockPositions(start, direction, length)) { foreach (var pos in GetBlockPositions(start, direction, length)) {
if (world.GetBlockAt(pos) != null) continue; if (world.GetBlockAt(pos) != null) continue;
var color = Player.Color.Blend(Color.FromHsv(0.0F, 0.0F, GD.Randf(), 0.2F)); var color = Player.Color.Blend(Color.FromHsv(0.0F, 0.0F, GD.Randf(), 0.2F));
RPC.Reliable(world.SpawnBlock, pos.X, pos.Y, color, false); RPC.Reliable(world.GetPlayersTracking(pos.ToChunkPos(), true),
world.SpawnBlock, pos.X, pos.Y, color, false);
} }
} }
@ -146,7 +145,8 @@ public class CreativeBuilding : Node2D
foreach (var pos in GetBlockPositions(start, direction, length)) { foreach (var pos in GetBlockPositions(start, direction, length)) {
var block = world.GetBlockAt(pos); var block = world.GetBlockAt(pos);
if (block?.Unbreakable != false) continue; if (block?.Unbreakable != false) continue;
RPC.Reliable(world.Despawn, world.GetPathTo(block)); RPC.Reliable(world.GetPlayersTracking(pos.ToChunkPos(), true),
world.DespawnBlock, pos.X, pos.Y);
} }
} }
} }

@ -52,7 +52,7 @@ public class Items : Node2D, IItems
} }
public IEnumerator<Node2D> GetEnumerator() public IEnumerator<Node2D> GetEnumerator()
=> GetChildren().Cast<Node2D>().GetEnumerator(); => this.GetChildren<Node2D>().GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator(); => GetEnumerator();

@ -130,15 +130,15 @@ public class Weapon : Sprite
// CREDIT to lizzie for helping me figure out this trigonometry problem. // CREDIT to lizzie for helping me figure out this trigonometry problem.
var a = TipOffset.y * ((Scale.y > 0) ? 1 : -1); var a = TipOffset.y * ((Scale.y > 0) ? 1 : -1);
var c = Player.Position.DistanceTo(Cursor.Position); var c = Player.GlobalPosition.DistanceTo(Cursor.Position);
if (c < TipOffset.x) { if (c < TipOffset.x) {
// If the cursor is too close to the player, put the // If the cursor is too close to the player, put the
// weapon in a "lowered" state, where it can't be shot. // weapon in a "lowered" state, where it can't be shot.
AimDirection = Mathf.Deg2Rad((Cursor.Position.x > Player.Position.x) ? 30 : 150); AimDirection = Mathf.Deg2Rad((Cursor.Position.x > Player.GlobalPosition.x) ? 30 : 150);
_lowered = true; _lowered = true;
} else { } else {
var angleC = Mathf.Asin(a / c); var angleC = Mathf.Asin(a / c);
AimDirection = Cursor.Position.AngleToPoint(Player.Position) - angleC; AimDirection = Cursor.Position.AngleToPoint(Player.GlobalPosition) - angleC;
_lowered = false; _lowered = false;
} }
@ -201,7 +201,7 @@ public class Weapon : Sprite
var spread = (Mathf.Deg2Rad(Spread) + _currentSpreadInc) * Mathf.Clamp(random.NextGaussian(0.4F), -1, 1); var spread = (Mathf.Deg2Rad(Spread) + _currentSpreadInc) * Mathf.Clamp(random.NextGaussian(0.4F), -1, 1);
var dir = Mathf.Polar2Cartesian(1, angle + spread); var dir = Mathf.Polar2Cartesian(1, angle + spread);
var color = new Color(Player.Color, BulletOpacity); var color = new Color(Player.Color, BulletOpacity);
var bullet = new Bullet(Player.Position + tip, dir, EffectiveRange, MaximumRange, var bullet = new Bullet(Player.GlobalPosition + tip, dir, EffectiveRange, MaximumRange,
BulletVelocity, Damage / BulletsPerShot, color); BulletVelocity, Damage / BulletsPerShot, color);
this.GetWorld().AddChild(bullet); this.GetWorld().AddChild(bullet);
} }
@ -224,6 +224,7 @@ public class Weapon : Sprite
if (Player.NetworkID != GetTree().GetRpcSenderId()) return; if (Player.NetworkID != GetTree().GetRpcSenderId()) return;
if (float.IsNaN(aimDirection = Mathf.PosMod(aimDirection, Mathf.Tau))) return; if (float.IsNaN(aimDirection = Mathf.PosMod(aimDirection, Mathf.Tau))) return;
// TODO: Only send to players who can see the full path of the bullet.
if (FireInternal(aimDirection, toRight, seed)) if (FireInternal(aimDirection, toRight, seed))
RPC.Reliable(SendFire, aimDirection, toRight, seed); RPC.Reliable(SendFire, aimDirection, toRight, seed);
} else if (!(Player is LocalPlayer)) } else if (!(Player is LocalPlayer))

@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
public class PlayerVisibilityTracker
{
public const int TRACK_RANGE = 4;
public const int UNTRACK_RANGE = 5;
private static readonly List<(int, int)> _removedChunks
= new List<(int, int)>((UNTRACK_RANGE * 2 + 1) * (UNTRACK_RANGE * 2 + 1));
private readonly HashSet<(int X, int Y)> _trackingChunks = new HashSet<(int, int)>();
private (int, int)? _previousChunkPos;
public event Action<(int, int)> ChunkTracked;
public event Action<(int, int)> ChunkUntracked;
public bool IsChunkTracked((int, int) chunkPos)
=> _trackingChunks.Contains(chunkPos);
public void Process(Player player)
{
var chunkPos = BlockPos.FromVector(player.GlobalPosition).ToChunkPos();
if (chunkPos == _previousChunkPos) return;
bool IsWithin((int X, int Y) pos, int range)
=> (pos.X >= chunkPos.X - UNTRACK_RANGE) && (pos.X <= chunkPos.X + UNTRACK_RANGE) &&
(pos.Y >= chunkPos.Y - UNTRACK_RANGE) && (pos.Y <= chunkPos.Y + UNTRACK_RANGE);
foreach (var pos in _trackingChunks)
if (!IsWithin(pos, UNTRACK_RANGE))
_removedChunks.Add(pos);
foreach (var pos in _removedChunks) {
_trackingChunks.Remove(pos);
ChunkUntracked?.Invoke(pos);
}
_removedChunks.Clear();
for (var x = chunkPos.X - TRACK_RANGE; x <= chunkPos.X + TRACK_RANGE; x++)
for (var y = chunkPos.Y - TRACK_RANGE; y <= chunkPos.Y + TRACK_RANGE; y++)
if (_trackingChunks.Add((x, y)))
ChunkTracked?.Invoke((x, y));
_previousChunkPos = chunkPos;
}
public void Reset()
{
_trackingChunks.Clear();
_previousChunkPos = null;
}
}

@ -2,8 +2,11 @@ using Godot;
public class Block : StaticBody2D, IInitializable public class Block : StaticBody2D, IInitializable
{ {
public new BlockPos Position { get => BlockPos.FromVector(base.Position); public const int LENGTH = 16;
set => base.Position = value.ToVector(); } public const int BIT_SHIFT = 4;
public BlockPos GlobalBlockPos { get => BlockPos.FromVector(GlobalPosition); set => GlobalPosition = value.ToVector(); }
public BlockPos ChunkLocalBlockPos { get => BlockPos.FromVector(Position); set => Position = value.ToVector(); }
public Color Color { get => Sprite.SelfModulate; set => Sprite.SelfModulate = value; } public Color Color { get => Sprite.SelfModulate; set => Sprite.SelfModulate = value; }
public bool Unbreakable { get; set; } = false; public bool Unbreakable { get; set; } = false;

@ -38,7 +38,8 @@ public class Bullet : Node2D
var world = this.GetWorld(); var world = this.GetWorld();
var path = world.GetPathTo(sprite); var path = world.GetPathTo(sprite);
var color = new Color(Color, (1 + Color.a) / 2); var color = new Color(Color, (1 + Color.a) / 2);
RPC.Reliable(world.SpawnHit, path, hitPosition, color); RPC.Reliable(world.GetPlayersTracking(BlockPos.FromVector(obj.GlobalPosition).ToChunkPos()),
world.SpawnHit, path, hitPosition, color);
if (obj is Player player) { if (obj is Player player) {
var rangeFactor = Math.Min(1.0F, (MaximumRange - _distance) / (MaximumRange - EffectiveRange)); var rangeFactor = Math.Min(1.0F, (MaximumRange - _distance) / (MaximumRange - EffectiveRange));
player.Health -= Damage * rangeFactor; player.Health -= Damage * rangeFactor;
@ -71,7 +72,7 @@ public class Bullet : Node2D
Position = (Vector2)collision["position"]; Position = (Vector2)collision["position"];
_distance = _startPosition.DistanceTo(Position); _distance = _startPosition.DistanceTo(Position);
var obj = (CollisionObject2D)collision["collider"]; var obj = (CollisionObject2D)collision["collider"];
OnCollide(obj, Position - obj.Position); OnCollide(obj, Position - obj.GlobalPosition);
SetPhysicsProcess(false); SetPhysicsProcess(false);
} }

@ -25,6 +25,8 @@ public class Player : KinematicBody2D, IInitializable
private float _regenDelay; private float _regenDelay;
private float _respawnDelay; private float _respawnDelay;
public PlayerVisibilityTracker VisibilityTracker { get; } = new PlayerVisibilityTracker();
public void Initialize() public void Initialize()
{ {
DisplayNameLabel = GetNode<Label>(DisplayNamePath); DisplayNameLabel = GetNode<Label>(DisplayNamePath);
@ -43,7 +45,9 @@ public class Player : KinematicBody2D, IInitializable
public override void _Process(float delta) public override void _Process(float delta)
{ {
if (this.GetGame() is Server) { if (!(this.GetGame() is Server)) return;
// Damage player when falling into "the void", so they can respawn.
if (Position.y > 9000) Health -= 0.01F; if (Position.y > 9000) Health -= 0.01F;
if (IsAlive && (Health < 1.0F)) { if (IsAlive && (Health < 1.0F)) {
@ -71,7 +75,8 @@ public class Player : KinematicBody2D, IInitializable
Rset("modulate", new Color(0.35F, 0.35F, 0.35F, 0.8F)); Rset("modulate", new Color(0.35F, 0.35F, 0.35F, 0.8F));
_previousHealth = Health; _previousHealth = Health;
} }
}
VisibilityTracker.Process(this);
} }

@ -65,7 +65,7 @@ public class Client : Game
} }
} else { } else {
this.GetWorld().ClearPlayers(); this.GetWorld().ClearPlayers();
this.GetWorld().ClearBlocks(); this.GetWorld().ClearChunks();
} }
Disconnected?.Invoke(); Disconnected?.Invoke();

@ -93,11 +93,6 @@ public class Server : Game
((Items)player.Items).DoSetCurrent, player.Items.Current.Name); ((Items)player.Items).DoSetCurrent, player.Items.Current.Name);
} }
foreach (var block in world.Blocks)
RPC.Reliable(networkID, world.SpawnBlock,
block.Position.X, block.Position.Y,
block.Color, block.Unbreakable);
RPC.Reliable(world.SpawnPlayer, networkID, Vector2.Zero); RPC.Reliable(world.SpawnPlayer, networkID, Vector2.Zero);
if (IsSingleplayer) LocalPlayer = world.GetPlayer(networkID); if (IsSingleplayer) LocalPlayer = world.GetPlayer(networkID);
} }

@ -15,11 +15,25 @@ public readonly struct BlockPos : IEquatable<BlockPos>
public int Y { get; } public int Y { get; }
public BlockPos(int x, int y) { X = x; Y = y; } public BlockPos(int x, int y) { X = x; Y = y; }
public static BlockPos FromVector(Vector2 vec) public static BlockPos FromVector(Vector2 vec) => new BlockPos(
=> new BlockPos(Mathf.RoundToInt(vec.x / 16), Mathf.RoundToInt(vec.y / 16)); Mathf.RoundToInt(vec.x / Block.LENGTH), Mathf.RoundToInt(vec.y / Block.LENGTH));
public BlockPos Add(int x, int y) => new BlockPos(X + x, Y + y);
public BlockPos Add(Facing facing, int distance = 1)
{ var (x, y) = facing; return Add(x * distance, y * distance); }
public BlockPos Subtract(int x, int y) => new BlockPos(X - x, Y - y);
public BlockPos Subtract(Facing facing, int distance = 1)
{ var (x, y) = facing; return Subtract(x * distance, y * distance); }
public BlockPos GlobalToChunkRel()
=> new BlockPos(X & Chunk.BIT_MASK, Y & Chunk.BIT_MASK);
public BlockPos ChunkRelToGlobal((int X, int Y) chunkPos)
=> new BlockPos(chunkPos.X << Chunk.BIT_SHIFT | X, chunkPos.Y << Chunk.BIT_SHIFT | Y);
public void Deconstruct(out int x, out int y) { x = X; y = Y; } public void Deconstruct(out int x, out int y) { x = X; y = Y; }
public Vector2 ToVector() => new Vector2(X * 16, Y * 16); public Vector2 ToVector() => new Vector2(X << Block.BIT_SHIFT, Y << Block.BIT_SHIFT);
public (int X, int Y) ToChunkPos() => (X >> Chunk.BIT_SHIFT, Y >> Chunk.BIT_SHIFT);
public override string ToString() => $"({X}, {Y})"; public override string ToString() => $"({X}, {Y})";
public override bool Equals(object obj) => (obj is BlockPos other) && Equals(other); public override bool Equals(object obj) => (obj is BlockPos other) && Equals(other);
@ -42,8 +56,4 @@ public readonly struct BlockPos : IEquatable<BlockPos>
=> new BlockPos(left.X + right.X, left.Y + right.Y); => new BlockPos(left.X + right.X, left.Y + right.Y);
public static BlockPos operator -(BlockPos left, BlockPos right) public static BlockPos operator -(BlockPos left, BlockPos right)
=> new BlockPos(left.X - right.X, left.Y - right.Y); => new BlockPos(left.X - right.X, left.Y - right.Y);
public static BlockPos operator *(BlockPos left, int right)
=> new BlockPos(left.X * right, left.Y * right);
public static BlockPos operator /(BlockPos left, int right)
=> new BlockPos(left.X / right, left.Y / right);
} }

@ -1,4 +1,6 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using Godot; using Godot;
public static class Extensions public static class Extensions
@ -20,6 +22,19 @@ public static class Extensions
public static World GetWorld(this Node node) public static World GetWorld(this Node node)
=> node.GetGame().GetNode<World>("World"); => node.GetGame().GetNode<World>("World");
public static IEnumerable<T> GetChildren<T>(this Node node)
=> node.GetChildren().Cast<T>();
public static T GetOrCreateChild<T>(this Node node, string name, Func<T> createFunc)
where T : Node
{
var child = node.GetNodeOrNull<T>(name);
if (child == null) {
child = createFunc();
child.Name = name;
node.AddChild(child);
}
return child;
}
public static void RemoveFromParent(this Node node) public static void RemoveFromParent(this Node node)
{ {
node.GetParent().RemoveChild(node); node.GetParent().RemoveChild(node);
@ -38,6 +53,9 @@ public static class Extensions
var normal = Mathf.Sqrt(-2.0F * Mathf.Log(u1)) * Mathf.Sin(2.0F * Mathf.Pi * u2); var normal = Mathf.Sqrt(-2.0F * Mathf.Log(u1)) * Mathf.Sin(2.0F * Mathf.Pi * u2);
return mean + stdDev * normal; return mean + stdDev * normal;
} }
public static void Deconstruct<TKey, TValue>(this KeyValuePair<TKey, TValue> kvp, out TKey key, out TValue value)
{ key = kvp.Key; value = kvp.Value; }
} }
public interface IInitializable public interface IInitializable

@ -16,28 +16,20 @@ public static class Facings
public static readonly IReadOnlyCollection<Facing> Horizontal = new []{ Facing.Left, Facing.Right }; public static readonly IReadOnlyCollection<Facing> Horizontal = new []{ Facing.Left, Facing.Right };
public static readonly IReadOnlyCollection<Facing> Vertical = new []{ Facing.Up, Facing.Down }; public static readonly IReadOnlyCollection<Facing> Vertical = new []{ Facing.Up, Facing.Down };
public static BlockPos ToBlockPos(this Facing facing) public static void Deconstruct(this Facing facing, out int x, out int y)
{ {
switch (facing) { switch (facing) {
case Facing.Left: return BlockPos.Left; case Facing.Left: x = -1; y = 0; break;
case Facing.Right: return BlockPos.Right; case Facing.Right: x = 1; y = 0; break;
case Facing.Up: return BlockPos.Up; case Facing.Up: x = 0; y = -1; break;
case Facing.Down: return BlockPos.Down; case Facing.Down: x = 0; y = 1; break;
default: throw new ArgumentException(); default: throw new ArgumentException();
} }
} }
public static Vector2 ToVector(this Facing facing) public static Vector2 ToVector(this Facing facing)
=> facing.ToBlockPos().ToVector(); { var (x, y) = facing; return new Vector2(x, y); }
public static BlockPos ToBlockPos(this Facing facing)
public static Facing FromAngle(float radians) { var (x, y) = facing; return new BlockPos(x, y); }
{
radians = Mathf.PosMod(radians, Mathf.Tau);
if (radians < Mathf.Pi / 4) return Facing.Right;
else if (radians < Mathf.Pi / 4 * 3) return Facing.Down;
else if (radians < Mathf.Pi / 4 * 5) return Facing.Left;
else if (radians < Mathf.Pi / 4 * 7) return Facing.Up;
else return Facing.Right;
}
public static float ToAngle(this Facing facing) public static float ToAngle(this Facing facing)
{ {
@ -49,4 +41,14 @@ public static class Facings
default: throw new ArgumentException(); default: throw new ArgumentException();
} }
} }
public static Facing FromAngle(float radians)
{
radians = Mathf.PosMod(radians, Mathf.Tau);
if (radians < Mathf.Pi / 4) return Facing.Right;
else if (radians < Mathf.Pi / 4 * 3) return Facing.Down;
else if (radians < Mathf.Pi / 4 * 5) return Facing.Left;
else if (radians < Mathf.Pi / 4 * 7) return Facing.Up;
else return Facing.Right;
}
} }

@ -1,8 +1,18 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using Godot; using Godot;
public static class RPC public static class RPC
{ {
public static IEnumerable<int> GetPlayersTracking(
this World world, (int, int) chunkPos, bool includeServer = false)
{
var ids = world.Players.Where(player => player.VisibilityTracker.IsChunkTracked(chunkPos))
.Select(player => player.NetworkID);
return includeServer ? ids.Prepend(1) : ids;
}
public static void Reliable(Action action) => GetNode(action).Rpc(action.Method.Name); public static void Reliable(Action action) => GetNode(action).Rpc(action.Method.Name);
public static void Reliable<T>(Action<T> action, T arg) => GetNode(action).Rpc(action.Method.Name, arg); public static void Reliable<T>(Action<T> action, T arg) => GetNode(action).Rpc(action.Method.Name, arg);
public static void Reliable<T0, T1>(Action<T0, T1> action, T0 arg0, T1 arg1) => GetNode(action).Rpc(action.Method.Name, arg0, arg1); public static void Reliable<T0, T1>(Action<T0, T1> action, T0 arg0, T1 arg1) => GetNode(action).Rpc(action.Method.Name, arg0, arg1);
@ -21,6 +31,15 @@ public static class RPC
public static void Reliable<T0, T1, T2, T3, T4, T5>(int networkID, Action<T0, T1, T2, T3, T4, T5> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => GetNode(action).RpcId(networkID, action.Method.Name, arg0, arg1, arg2, arg3, arg4, arg5); public static void Reliable<T0, T1, T2, T3, T4, T5>(int networkID, Action<T0, T1, T2, T3, T4, T5> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => GetNode(action).RpcId(networkID, action.Method.Name, arg0, arg1, arg2, arg3, arg4, arg5);
public static void Reliable<T0, T1, T2, T3, T4, T5, T6>(int networkID, Action<T0, T1, T2, T3, T4, T5, T6> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => GetNode(action).RpcId(networkID, action.Method.Name, arg0, arg1, arg2, arg3, arg4, arg5, arg6); public static void Reliable<T0, T1, T2, T3, T4, T5, T6>(int networkID, Action<T0, T1, T2, T3, T4, T5, T6> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => GetNode(action).RpcId(networkID, action.Method.Name, arg0, arg1, arg2, arg3, arg4, arg5, arg6);
public static void Reliable(IEnumerable<int> networkIDs, Action action) { var node = GetNode(action); foreach (var id in networkIDs) node.RpcId(id, action.Method.Name); }
public static void Reliable<T>(IEnumerable<int> networkIDs, Action<T> action, T arg) { var node = GetNode(action); foreach (var id in networkIDs) node.RpcId(id, action.Method.Name, arg); }
public static void Reliable<T0, T1>(IEnumerable<int> networkIDs, Action<T0, T1> action, T0 arg0, T1 arg1) { var node = GetNode(action); foreach (var id in networkIDs) node.RpcId(id, action.Method.Name, arg0, arg1); }
public static void Reliable<T0, T1, T2>(IEnumerable<int> networkIDs, Action<T0, T1, T2> action, T0 arg0, T1 arg1, T2 arg2) { var node = GetNode(action); foreach (var id in networkIDs) node.RpcId(id, action.Method.Name, arg0, arg1, arg2); }
public static void Reliable<T0, T1, T2, T3>(IEnumerable<int> networkIDs, Action<T0, T1, T2, T3> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3) { var node = GetNode(action); foreach (var id in networkIDs) node.RpcId(id, action.Method.Name, arg0, arg1, arg2, arg3); }
public static void Reliable<T0, T1, T2, T3, T4>(IEnumerable<int> networkIDs, Action<T0, T1, T2, T3, T4> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4) { var node = GetNode(action); foreach (var id in networkIDs) node.RpcId(id, action.Method.Name, arg0, arg1, arg2, arg3, arg4); }
public static void Reliable<T0, T1, T2, T3, T4, T5>(IEnumerable<int> networkIDs, Action<T0, T1, T2, T3, T4, T5> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) { var node = GetNode(action); foreach (var id in networkIDs) node.RpcId(id, action.Method.Name, arg0, arg1, arg2, arg3, arg4, arg5); }
public static void Reliable<T0, T1, T2, T3, T4, T5, T6>(IEnumerable<int> networkIDs, Action<T0, T1, T2, T3, T4, T5, T6> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) { var node = GetNode(action); foreach (var id in networkIDs) node.RpcId(id, action.Method.Name, arg0, arg1, arg2, arg3, arg4, arg5, arg6); }
public static void Unreliable(Action action) => GetNode(action).RpcUnreliable(action.Method.Name); public static void Unreliable(Action action) => GetNode(action).RpcUnreliable(action.Method.Name);
public static void Unreliable<T>(Action<T> action, T arg) => GetNode(action).RpcUnreliable(action.Method.Name, arg); public static void Unreliable<T>(Action<T> action, T arg) => GetNode(action).RpcUnreliable(action.Method.Name, arg);
public static void Unreliable<T0, T1>(Action<T0, T1> action, T0 arg0, T1 arg1) => GetNode(action).RpcUnreliable(action.Method.Name, arg0, arg1); public static void Unreliable<T0, T1>(Action<T0, T1> action, T0 arg0, T1 arg1) => GetNode(action).RpcUnreliable(action.Method.Name, arg0, arg1);
@ -39,6 +58,15 @@ public static class RPC
public static void Unreliable<T0, T1, T2, T3, T4, T5>(int networkID, Action<T0, T1, T2, T3, T4, T5> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => GetNode(action).RpcUnreliableId(networkID, action.Method.Name, arg0, arg1, arg2, arg3, arg4, arg5); public static void Unreliable<T0, T1, T2, T3, T4, T5>(int networkID, Action<T0, T1, T2, T3, T4, T5> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => GetNode(action).RpcUnreliableId(networkID, action.Method.Name, arg0, arg1, arg2, arg3, arg4, arg5);
public static void Unreliable<T0, T1, T2, T3, T4, T5, T6>(int networkID, Action<T0, T1, T2, T3, T4, T5, T6> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => GetNode(action).RpcUnreliableId(networkID, action.Method.Name, arg0, arg1, arg2, arg3, arg4, arg5, arg6); public static void Unreliable<T0, T1, T2, T3, T4, T5, T6>(int networkID, Action<T0, T1, T2, T3, T4, T5, T6> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => GetNode(action).RpcUnreliableId(networkID, action.Method.Name, arg0, arg1, arg2, arg3, arg4, arg5, arg6);
public static void Unreliable(IEnumerable<int> networkIDs, Action action) { var node = GetNode(action); foreach (var id in networkIDs) node.RpcUnreliableId(id, action.Method.Name); }
public static void Unreliable<T>(IEnumerable<int> networkIDs, Action<T> action, T arg) { var node = GetNode(action); foreach (var id in networkIDs) node.RpcUnreliableId(id, action.Method.Name, arg); }
public static void Unreliable<T0, T1>(IEnumerable<int> networkIDs, Action<T0, T1> action, T0 arg0, T1 arg1) { var node = GetNode(action); foreach (var id in networkIDs) node.RpcUnreliableId(id, action.Method.Name, arg0, arg1); }
public static void Unreliable<T0, T1, T2>(IEnumerable<int> networkIDs, Action<T0, T1, T2> action, T0 arg0, T1 arg1, T2 arg2) { var node = GetNode(action); foreach (var id in networkIDs) node.RpcUnreliableId(id, action.Method.Name, arg0, arg1, arg2); }
public static void Unreliable<T0, T1, T2, T3>(IEnumerable<int> networkIDs, Action<T0, T1, T2, T3> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3) { var node = GetNode(action); foreach (var id in networkIDs) node.RpcUnreliableId(id, action.Method.Name, arg0, arg1, arg2, arg3); }
public static void Unreliable<T0, T1, T2, T3, T4>(IEnumerable<int> networkIDs, Action<T0, T1, T2, T3, T4> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4) { var node = GetNode(action); foreach (var id in networkIDs) node.RpcUnreliableId(id, action.Method.Name, arg0, arg1, arg2, arg3, arg4); }
public static void Unreliable<T0, T1, T2, T3, T4, T5>(IEnumerable<int> networkIDs, Action<T0, T1, T2, T3, T4, T5> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) { var node = GetNode(action); foreach (var id in networkIDs) node.RpcUnreliableId(id, action.Method.Name, arg0, arg1, arg2, arg3, arg4, arg5); }
public static void Unreliable<T0, T1, T2, T3, T4, T5, T6>(IEnumerable<int> networkIDs, Action<T0, T1, T2, T3, T4, T5, T6> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) { var node = GetNode(action); foreach (var id in networkIDs) node.RpcUnreliableId(id, action.Method.Name, arg0, arg1, arg2, arg3, arg4, arg5, arg6); }
private static Node GetNode(Delegate action) => (action.Target as Node) ?? throw new ArgumentException( private static Node GetNode(Delegate action) => (action.Target as Node) ?? throw new ArgumentException(
$"Target ({action.Target?.GetType().ToString() ?? "null"}) must be a Node", nameof(action)); $"Target ({action.Target?.GetType().ToString() ?? "null"}) must be a Node", nameof(action));
} }

@ -1,69 +1,90 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Godot; using Godot;
public class World : Node public class World : Node
{ {
[Export] public NodePath PlayerContainerPath { get; set; } private static readonly PackedScene BLOCK = GD.Load<PackedScene>("res://scene/Block.tscn");
[Export] public NodePath BlockContainerPath { get; set; } private static readonly PackedScene PLAYER = GD.Load<PackedScene>("res://scene/Player.tscn");
private static readonly PackedScene LOCAL_PLAYER = GD.Load<PackedScene>("res://scene/LocalPlayer.tscn");
public Node PlayerContainer { get; private set; }
public Node BlockContainer { get; private set; }
public PackedScene BlockScene { get; private set; }
public PackedScene PlayerScene { get; private set; }
public PackedScene LocalPlayerScene { get; private set; }
private static readonly PackedScene HIT_DECAL = GD.Load<PackedScene>("res://scene/HitDecal.tscn"); private static readonly PackedScene HIT_DECAL = GD.Load<PackedScene>("res://scene/HitDecal.tscn");
// TODO: Make all of these static and readonly, hardcode the values..?
public override void _Ready() internal Node PlayerContainer { get; }
{ internal Node ChunkContainer { get; }
PlayerContainer = GetNode(PlayerContainerPath);
BlockContainer = GetNode(BlockContainerPath);
BlockScene = GD.Load<PackedScene>("res://scene/Block.tscn"); public World()
PlayerScene = GD.Load<PackedScene>("res://scene/Player.tscn"); {
LocalPlayerScene = GD.Load<PackedScene>("res://scene/LocalPlayer.tscn"); AddChild(PlayerContainer = new Node { Name = "Players" });
AddChild(ChunkContainer = new Node { Name = "Chunks" });
} }
public IEnumerable<Player> Players public IEnumerable<Player> Players
=> PlayerContainer.GetChildren().Cast<Player>(); => PlayerContainer.GetChildren<Player>();
public Player GetPlayer(int networkID) public Player GetPlayer(int networkID)
=> PlayerContainer.GetNodeOrNull<Player>(networkID.ToString()); => PlayerContainer.GetNodeOrNull<Player>(networkID.ToString());
public void ClearPlayers() public void ClearPlayers()
{ foreach (var player in Players) player.RemoveFromParent(); } { foreach (var player in Players) player.RemoveFromParent(); }
public IEnumerable<Block> Blocks public IEnumerable<Chunk> Chunks
=> BlockContainer.GetChildren().Cast<Block>(); => ChunkContainer.GetChildren<Chunk>();
public Block GetBlockAt(BlockPos position) public Chunk GetChunkOrNull((int X, int Y) chunkPos)
=> BlockContainer.GetNodeOrNull<Block>(position.ToString()); => ChunkContainer.GetNodeOrNull<Chunk>($"Chunk ({chunkPos.X}, {chunkPos.Y})");
[PuppetSync] public void ClearBlocks() public Chunk GetOrCreateChunk((int X, int Y) chunkPos)
{ foreach (var block in Blocks) block.RemoveFromParent(); } => ChunkContainer.GetOrCreateChild($"Chunk ({chunkPos.X}, {chunkPos.Y})", () => new Chunk(chunkPos.X, chunkPos.Y));
[PuppetSync] public void ClearChunks()
{ foreach (var chunk in Chunks) chunk.RemoveFromParent(); }
public Block GetBlockAt(BlockPos position)
=> GetChunkOrNull(position.ToChunkPos())
?.GetLayerOrNull<Block>()?[position.GlobalToChunkRel()];
[PuppetSync] [PuppetSync]
public void SpawnBlock(int x, int y, Color color, bool unbreakable) public void SpawnBlock(int x, int y, Color color, bool unbreakable)
{ {
var position = new BlockPos(x, y); var blockPos = new BlockPos(x, y);
var block = BlockScene.Init<Block>(); var block = BLOCK.Init<Block>();
block.Name = position.ToString(); block.Name = blockPos.ToString();
block.Position = position;
block.Color = color; block.Color = color;
block.Unbreakable = unbreakable; block.Unbreakable = unbreakable;
BlockContainer.AddChild(block); block.ChunkLocalBlockPos = blockPos.GlobalToChunkRel();
GetOrCreateChunk(blockPos.ToChunkPos())
.GetOrCreateLayer<Block>()[block.ChunkLocalBlockPos] = block;
}
[PuppetSync]
public void DespawnBlock(int x, int y)
{
var blockPos = new BlockPos(x, y);
var blockLayer = GetChunkOrNull(blockPos.ToChunkPos())?.GetLayerOrNull<Block>();
if (blockLayer != null) blockLayer[blockPos.GlobalToChunkRel()] = null;
} }
[PuppetSync] [PuppetSync]
public void SpawnPlayer(int networkID, Vector2 position) public void SpawnPlayer(int networkID, Vector2 position)
{ {
var isLocal = networkID == GetTree().GetNetworkUniqueId(); var isLocal = networkID == GetTree().GetNetworkUniqueId();
var player = (isLocal ? LocalPlayerScene : PlayerScene).Init<Player>(); var player = (isLocal ? LOCAL_PLAYER : PLAYER).Init<Player>();
player.NetworkID = networkID; player.NetworkID = networkID;
player.Position = position; player.Position = position;
PlayerContainer.AddChild(player); PlayerContainer.AddChild(player);
if (player is LocalPlayer localPlayer) if (player is LocalPlayer localPlayer)
this.GetClient().FireLocalPlayerSpawned(localPlayer); this.GetClient().FireLocalPlayerSpawned(localPlayer);
if (this.GetGame() is Server) {
player.VisibilityTracker.ChunkTracked += (chunkPos) => {
var chunk = GetChunkOrNull(chunkPos);
if (chunk == null) return;
foreach (var block in chunk.GetLayerOrNull<Block>().GetChildren<Block>())
RPC.Reliable(player.NetworkID, SpawnBlock,
block.GlobalBlockPos.X, block.GlobalBlockPos.Y,
block.Color, block.Unbreakable);
};
player.VisibilityTracker.ChunkUntracked += (chunkPos) => {
var chunk = GetChunkOrNull(chunkPos);
if (chunk == null) return;
RPC.Reliable(player.NetworkID, Despawn, GetPathTo(chunk));
};
}
} }
[Puppet] [Puppet]

Loading…
Cancel
Save