From 7c4ae2ce456af53641ba9f48e119c01d627bdda3 Mon Sep 17 00:00:00 2001 From: copygirl Date: Sat, 22 May 2021 19:37:37 +0200 Subject: [PATCH] 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 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 --- scene/GameScene.tscn | 8 +-- scene/HitDecal.tscn | 53 ++++++++-------- src/Chunk.cs | 72 ++++++++++++++++++++++ src/EscapeMenuMultiplayer.cs | 2 +- src/EscapeMenuWorld.cs | 7 ++- src/IO/WorldSave.cs | 75 ++++++++++++++++------- src/Items/CreativeBuilding.cs | 14 ++--- src/Items/Items.cs | 2 +- src/Items/Weapon.cs | 9 +-- src/Network/PlayerVisibilityTracker.cs | 52 ++++++++++++++++ src/Objects/Block.cs | 7 ++- src/Objects/Bullet.cs | 5 +- src/Objects/Player.cs | 57 ++++++++++-------- src/Scenes/Client.cs | 2 +- src/Scenes/Server.cs | 5 -- src/Utility/BlockPos.cs | 24 +++++--- src/Utility/Extensions.cs | 18 ++++++ src/Utility/Facing.cs | 34 ++++++----- src/Utility/RPC.cs | 28 +++++++++ src/World.cs | 83 ++++++++++++++++---------- 20 files changed, 396 insertions(+), 161 deletions(-) create mode 100644 src/Chunk.cs create mode 100644 src/Network/PlayerVisibilityTracker.cs diff --git a/scene/GameScene.tscn b/scene/GameScene.tscn index 0a24d21..3eef60a 100644 --- a/scene/GameScene.tscn +++ b/scene/GameScene.tscn @@ -9,9 +9,5 @@ script = ExtResource( 3 ) [node name="World" type="Node" parent="."] script = ExtResource( 1 ) -PlayerContainerPath = NodePath("Players") -BlockContainerPath = NodePath("Blocks") - -[node name="Players" type="Node" parent="World"] - -[node name="Blocks" type="Node" parent="World"] +PlayerContainerPath = NodePath("") +BlockContainerPath = NodePath("") diff --git a/scene/HitDecal.tscn b/scene/HitDecal.tscn index 7dbe597..02a91c3 100644 --- a/scene/HitDecal.tscn +++ b/scene/HitDecal.tscn @@ -3,16 +3,16 @@ [ext_resource path="res://gfx/hit_decal.png" type="Texture" id=1] [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" -[sub_resource type="VisualShaderNodeInput" id=16] +[sub_resource type="VisualShaderNodeInput" id=2] input_name = "modulate_alpha" -[sub_resource type="VisualShaderNodeScalarOp" id=17] +[sub_resource type="VisualShaderNodeScalarOp" id=3] operator = 2 -[sub_resource type="VisualShaderNodeExpression" id=18] +[sub_resource type="VisualShaderNodeExpression" id=4] size = Vector2( 512, 260 ) expression = "vec2 tex_size = vec2(textureSize(mask, 0)); 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 && 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" -[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 -[sub_resource type="VisualShaderNodeTexture" id=22] +[sub_resource type="VisualShaderNodeTexture" id=8] source = 5 -[sub_resource type="VisualShaderNodeInput" id=23] +[sub_resource type="VisualShaderNodeInput" id=9] input_name = "texture" -[sub_resource type="VisualShaderNodeScalarOp" id=24] +[sub_resource type="VisualShaderNodeScalarOp" id=10] operator = 2 -[sub_resource type="VisualShaderNodeVec3Uniform" id=25] +[sub_resource type="VisualShaderNodeVec3Uniform" id=11] uniform_name = "offset" -[sub_resource type="VisualShaderNodeTextureUniform" id=26] +[sub_resource type="VisualShaderNodeTextureUniform" id=12] uniform_name = "mask" -[sub_resource type="VisualShader" id=27] +[sub_resource type="VisualShader" id=13] code = "shader_type canvas_item; uniform vec3 offset; uniform sampler2D mask; @@ -133,27 +133,26 @@ void light() { } " -graph_offset = Vector2( 341, -275 ) mode = 1 flags/light_only = false 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/3/node = SubResource( 23 ) +nodes/fragment/3/node = SubResource( 9 ) 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/7/node = SubResource( 25 ) +nodes/fragment/7/node = SubResource( 11 ) 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/10/node = SubResource( 15 ) +nodes/fragment/10/node = SubResource( 1 ) 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/14/node = SubResource( 17 ) +nodes/fragment/14/node = SubResource( 3 ) 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/size = Vector2( 512, 260 ) 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); outside = mask_uv.x >= 0.0 && mask_uv.x <= 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/17/node = SubResource( 20 ) +nodes/fragment/17/node = SubResource( 6 ) 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/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] -shader = SubResource( 27 ) +shader = SubResource( 13 ) shader_param/offset = Vector3( 0, 0, 0 ) [node name="HitDecal" type="Sprite"] diff --git a/src/Chunk.cs b/src/Chunk.cs new file mode 100644 index 0000000..5f3f3cc --- /dev/null +++ b/src/Chunk.cs @@ -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 GetLayerOrNull() + => GetNodeOrNull>($"{typeof(T).Name}Layer"); + public ChunkLayer GetOrCreateLayer() + { + var layer = GetLayerOrNull(); + if (layer == null) AddChild(layer = new ChunkLayer { 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 : Node2D +{ + private static readonly IEqualityComparer COMPARER = EqualityComparer.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"); + } +} diff --git a/src/EscapeMenuMultiplayer.cs b/src/EscapeMenuMultiplayer.cs index 16850a6..aa2e5ba 100644 --- a/src/EscapeMenuMultiplayer.cs +++ b/src/EscapeMenuMultiplayer.cs @@ -123,7 +123,7 @@ public class EscapeMenuMultiplayer : Container if (server.IsRunning) { server.Stop(); server.GetWorld().ClearPlayers(); - server.GetWorld().ClearBlocks(); + server.GetWorld().ClearChunks(); client.Disconnect(); } diff --git a/src/EscapeMenuWorld.cs b/src/EscapeMenuWorld.cs index 82661da..55ddfdf 100644 --- a/src/EscapeMenuWorld.cs +++ b/src/EscapeMenuWorld.cs @@ -103,10 +103,13 @@ public class EscapeMenuWorld : CenterContainer var server = this.GetClient().GetNode(nameof(IntegratedServer)).Server; var save = WorldSave.ReadFromFile(path); - // Reset players' positions. - foreach (var player in server.GetWorld().Players) + foreach (var player in server.GetWorld().Players) { + // Reset players' positions. // Can't use RPC helper method here since player is not a LocalPlayer here. 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()); _playtime = save.Playtime; diff --git a/src/IO/WorldSave.cs b/src/IO/WorldSave.cs index ff6c586..d28caad 100644 --- a/src/IO/WorldSave.cs +++ b/src/IO/WorldSave.cs @@ -9,7 +9,7 @@ public class WorldSave { public const string FILE_EXT = ".yf5"; 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/"; @@ -18,7 +18,7 @@ public class WorldSave public int Version { get; private set; } = LATEST_VERSION; public TimeSpan Playtime { get; set; } = TimeSpan.Zero; - public List<(BlockPos, Color, bool)> Blocks { get; private set; } + public Dictionary<(int, int), Dictionary> Chunks { get; private set; } public static WorldSave ReadFromFile(string path) @@ -30,19 +30,34 @@ public class WorldSave if (magic != MAGIC_NUMBER) throw new IOException( $"Magic number does not match ({magic:X8} != {MAGIC_NUMBER:X8})"); - // TODO: See how to support multiple versions. - save.Version = reader.ReadUInt16(); - if (save.Version != LATEST_VERSION) throw new IOException( - $"Version does not match ({save.Version} != {LATEST_VERSION})"); - + // TODO: See how to better support multiple versions, improve saving/loading. + save.Version = reader.ReadUInt16(); save.Playtime = TimeSpan.FromSeconds(reader.ReadUInt32()); - var numBlocks = reader.ReadInt32(); - save.Blocks = new List<(BlockPos, Color, bool)>(); - for (var i = 0; i < numBlocks; i++) - save.Blocks.Add((new BlockPos(reader.ReadInt32(), reader.ReadInt32()), - new Color(reader.ReadInt32()), - reader.ReadBoolean())); + if (save.Version == 0) { + save.Chunks = new Dictionary<(int, int), Dictionary>(); + var numBlocks = reader.ReadInt32(); + for (var i = 0; i < numBlocks; i++) { + var blockPos = new BlockPos(reader.ReadInt32(), reader.ReadInt32()); + var blockData = (new Color(reader.ReadInt32()), reader.ReadBoolean()); + var chunkPos = blockPos.ToChunkPos(); + if (!save.Chunks.TryGetValue(chunkPos, out var blocks)) + save.Chunks.Add(chunkPos, blocks = new Dictionary()); + blocks.Add(blockPos.GlobalToChunkRel(), blockData); + } + } else if (save.Version == 1) { + var numChunks = reader.ReadInt32(); + save.Chunks = new Dictionary<(int, int), Dictionary>(numChunks); + for (var i = 0; i < numChunks; i++) { + var chunkPos = (reader.ReadInt32(), reader.ReadInt32()); + var numBlocks = (int)reader.ReadUInt16(); + var blocks = new Dictionary(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; @@ -56,12 +71,17 @@ public class WorldSave writer.Write((ushort)LATEST_VERSION); writer.Write((uint)Playtime.TotalSeconds); - writer.Write(Blocks.Count); - foreach (var (position, color, unbreakable) in Blocks) { - writer.Write(position.X); - writer.Write(position.Y); - writer.Write(color.ToRgba32()); - writer.Write(unbreakable); + writer.Write(Chunks.Count); + foreach (var ((chunkX, chunkY), blocks) in Chunks) { + writer.Write(chunkX); + 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(unbreakable); + } } } } @@ -71,12 +91,21 @@ public class WorldSave 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() + .GetChildren().ToDictionary( + block => block.ChunkLocalBlockPos, + block => (block.Color, block.Unbreakable))); public void ReadDataIntoWorld(World world) { - RPC.Reliable(world.ClearBlocks); - foreach (var (position, color, unbreakable) in Blocks) - RPC.Reliable(world.SpawnBlock, position.X, position.Y, color, unbreakable); + RPC.Reliable(world.ClearChunks); + foreach (var (chunkPos, blocks) in Chunks) { + foreach (var (blockPos, (color, unbreakable)) in blocks) { + var (x, y) = blockPos.ChunkRelToGlobal(chunkPos); + world.SpawnBlock(x, y, color, unbreakable); + } + } } } diff --git a/src/Items/CreativeBuilding.cs b/src/Items/CreativeBuilding.cs index 24d014b..78966c9 100644 --- a/src/Items/CreativeBuilding.cs +++ b/src/Items/CreativeBuilding.cs @@ -39,14 +39,12 @@ public class CreativeBuilding : Node2D if (ev.IsActionPressed("interact_primary")) { GetTree().SetInputAsHandled(); - _currentMode = (((_currentMode == null) && _canBuild) ? BuildMode.Placing : (BuildMode?)null); + _currentMode = ((_currentMode == null) && _canBuild) ? BuildMode.Placing : (BuildMode?)null; } if (ev.IsActionPressed("interact_secondary")) { 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) @@ -106,7 +104,7 @@ public class CreativeBuilding : Node2D } private static IEnumerable 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] @@ -127,7 +125,8 @@ public class CreativeBuilding : Node2D foreach (var pos in GetBlockPositions(start, direction, length)) { if (world.GetBlockAt(pos) != null) continue; 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)) { var block = world.GetBlockAt(pos); 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); } } } diff --git a/src/Items/Items.cs b/src/Items/Items.cs index 7bf91a4..3a8a9d7 100644 --- a/src/Items/Items.cs +++ b/src/Items/Items.cs @@ -52,7 +52,7 @@ public class Items : Node2D, IItems } public IEnumerator GetEnumerator() - => GetChildren().Cast().GetEnumerator(); + => this.GetChildren().GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); diff --git a/src/Items/Weapon.cs b/src/Items/Weapon.cs index e4d1f30..5d2cca3 100644 --- a/src/Items/Weapon.cs +++ b/src/Items/Weapon.cs @@ -130,15 +130,15 @@ public class Weapon : Sprite // CREDIT to lizzie for helping me figure out this trigonometry problem. 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 the cursor is too close to the player, put the // 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; } else { var angleC = Mathf.Asin(a / c); - AimDirection = Cursor.Position.AngleToPoint(Player.Position) - angleC; + AimDirection = Cursor.Position.AngleToPoint(Player.GlobalPosition) - angleC; _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 dir = Mathf.Polar2Cartesian(1, angle + spread); 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); this.GetWorld().AddChild(bullet); } @@ -224,6 +224,7 @@ public class Weapon : Sprite if (Player.NetworkID != GetTree().GetRpcSenderId()) 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)) RPC.Reliable(SendFire, aimDirection, toRight, seed); } else if (!(Player is LocalPlayer)) diff --git a/src/Network/PlayerVisibilityTracker.cs b/src/Network/PlayerVisibilityTracker.cs new file mode 100644 index 0000000..b933a38 --- /dev/null +++ b/src/Network/PlayerVisibilityTracker.cs @@ -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; + } +} diff --git a/src/Objects/Block.cs b/src/Objects/Block.cs index 817c1c0..8d3277d 100644 --- a/src/Objects/Block.cs +++ b/src/Objects/Block.cs @@ -2,8 +2,11 @@ using Godot; public class Block : StaticBody2D, IInitializable { - public new BlockPos Position { get => BlockPos.FromVector(base.Position); - set => base.Position = value.ToVector(); } + public const int LENGTH = 16; + 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 bool Unbreakable { get; set; } = false; diff --git a/src/Objects/Bullet.cs b/src/Objects/Bullet.cs index 26b2c9f..7baa1d0 100644 --- a/src/Objects/Bullet.cs +++ b/src/Objects/Bullet.cs @@ -38,7 +38,8 @@ public class Bullet : Node2D var world = this.GetWorld(); var path = world.GetPathTo(sprite); 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) { var rangeFactor = Math.Min(1.0F, (MaximumRange - _distance) / (MaximumRange - EffectiveRange)); player.Health -= Damage * rangeFactor; @@ -71,7 +72,7 @@ public class Bullet : Node2D Position = (Vector2)collision["position"]; _distance = _startPosition.DistanceTo(Position); var obj = (CollisionObject2D)collision["collider"]; - OnCollide(obj, Position - obj.Position); + OnCollide(obj, Position - obj.GlobalPosition); SetPhysicsProcess(false); } diff --git a/src/Objects/Player.cs b/src/Objects/Player.cs index 82bd1f5..2cba187 100644 --- a/src/Objects/Player.cs +++ b/src/Objects/Player.cs @@ -25,6 +25,8 @@ public class Player : KinematicBody2D, IInitializable private float _regenDelay; private float _respawnDelay; + public PlayerVisibilityTracker VisibilityTracker { get; } = new PlayerVisibilityTracker(); + public void Initialize() { DisplayNameLabel = GetNode