Re-implement block hit decals

- Implement BlockEntity more
- Add INotifyChildRemoved which
  works with RemoveFromParent
- IChunkLayer.Changed now reports BlockPos
main
copygirl 4 years ago
parent b7e7632c00
commit fe516c6ac6
  1. 14
      src/Objects/Bullet.cs
  2. 33
      src/Objects/HitDecal.cs
  3. 14
      src/Utility/Extensions.cs
  4. 15
      src/World/Block/BlockEntity.cs
  5. 6
      src/World/Block/BlockRef.cs
  6. 19
      src/World/Chunk/Chunk.cs
  7. 24
      src/World/Chunk/ChunkLayer.cs
  8. 9
      src/World/World.cs

@ -34,18 +34,24 @@ public class Bullet : Node2D
{ {
// TODO: Add a global game setting to specify whether shooter or server announces successful hit. // TODO: Add a global game setting to specify whether shooter or server announces successful hit.
// For now, server is the most straight-forward. Eventually, support client predictive movement? // For now, server is the most straight-forward. Eventually, support client predictive movement?
if (!(this.GetGame() is Server) || !(obj.GetNodeOrNull("Sprite") is Sprite sprite)) return; if (!(this.GetGame() is Server)) return;
var world = this.GetWorld(); var world = this.GetWorld();
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.GetPlayersTracking(BlockPos.FromVector(obj.GlobalPosition).ToChunkPos()), if (obj.GetParent() is Chunk chunk) {
world.SpawnHit, path, hitPosition, color); var path = world.GetPathTo(chunk);
var to = world.GetPlayersTracking(chunk.ChunkPos);
RPC.Reliable(to, world.SpawnHit, path, hitPosition, color);
} else if (obj.GetNodeOrNull("Sprite") is Sprite sprite) {
var path = world.GetPathTo(sprite);
var to = world.GetPlayersTracking(BlockPos.FromVector(obj.GlobalPosition).ToChunkPos());
RPC.Reliable(to, 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;
} }
// TODO: Also spawn a ghost of the player who was hit so they can see where they got shot? // TODO: Also spawn a ghost of the player who was hit so they can see where they got shot?
} }
}
public override void _Ready() public override void _Ready()
{ if (this.GetGame() is Server) Visible = false; } { if (this.GetGame() is Server) Visible = false; }

@ -34,4 +34,37 @@ public class HitDecal : Sprite
if (Modulate.a <= 0) this.RemoveFromParent(); if (Modulate.a <= 0) this.RemoveFromParent();
} }
} }
public static void Spawn(World world, NodePath path, Vector2 hitPosition, Color color)
{
var decal = GD.Load<Texture>("res://gfx/hit_decal.png");
var node = world.GetNode(path);
switch (node) {
case Sprite sprite:
node.AddChild(new HitDecal(decal, sprite.Texture, hitPosition, color));
break;
case Chunk chunk:
hitPosition += chunk.Position;
var start = BlockPos.FromVector((hitPosition - decal.GetSize() / 2).Floor());
var end = BlockPos.FromVector((hitPosition + decal.GetSize() / 2).Ceil());
for (var x = start.X; x <= end.X; x++)
for (var y = start.Y; y <= end.Y; y++) {
var blockPos = new BlockPos(x, y);
var texture = world[blockPos].Get<Block>().Texture;
if (texture == null) continue;
world[blockPos].GetOrCreate<HitDecals>().AddChild(new HitDecal(
decal, texture, hitPosition - blockPos.ToVector(), color));
}
break;
}
}
}
public class HitDecals : Node2D, INotifyChildRemoved
{
public void OnChildRemoved(Node child)
{
if (GetChildCount() == 0)
this.RemoveFromParent();
}
} }

@ -16,6 +16,10 @@ public static class Extensions
public static IEnumerable<T> GetChildren<T>(this Node node) public static IEnumerable<T> GetChildren<T>(this Node node)
=> node.GetChildren().Cast<T>(); => node.GetChildren().Cast<T>();
public static T GetOrCreateChild<T>(this Node node)
where T : Node, new() => node.GetOrCreateChild<T>(typeof(T).Name, () => new T());
public static T GetOrCreateChild<T>(this Node node, string name)
where T : Node, new() => node.GetOrCreateChild<T>(name, () => new T());
public static T GetOrCreateChild<T>(this Node node, string name, Func<T> createFunc) public static T GetOrCreateChild<T>(this Node node, string name, Func<T> createFunc)
where T : Node where T : Node
{ {
@ -39,8 +43,10 @@ public static class Extensions
} }
public static void RemoveFromParent(this Node node) public static void RemoveFromParent(this Node node)
{ {
var notifyParent = node.GetParent() as INotifyChildRemoved;
node.GetParent().RemoveChild(node); node.GetParent().RemoveChild(node);
node.QueueFree(); node.QueueFree();
notifyParent?.OnChildRemoved(node);
} }
public static float NextFloat(this Random random) public static float NextFloat(this Random random)
@ -59,3 +65,11 @@ public static class Extensions
public static void Deconstruct<TKey, TValue>(this KeyValuePair<TKey, TValue> kvp, out TKey key, out TValue value) public static void Deconstruct<TKey, TValue>(this KeyValuePair<TKey, TValue> kvp, out TKey key, out TValue value)
{ key = kvp.Key; value = kvp.Value; } { key = kvp.Key; value = kvp.Value; }
} }
/// <summary> When a child is removed from this Node using
/// <see cref="Extensions.RemoveFromParent"/>,
/// the OnChildRemoved method is called. </summary>
public interface INotifyChildRemoved
{
void OnChildRemoved(Node child);
}

@ -1,6 +1,19 @@
using Godot; using Godot;
public class BlockEntity : Node2D // TODO: Add saving of block entities.
public class BlockEntity : Node2D, INotifyChildRemoved
{ {
public BlockRef Block { get; }
public BlockEntity(BlockRef block)
{
Block = block;
Position = block.Position.GlobalToChunkRel().ToVector();
}
public void OnChildRemoved(Node child)
{
if (GetChildCount() == 0)
this.RemoveFromParent();
}
} }

@ -21,6 +21,8 @@ public class BlockRef
public BlockEntity GetEntity(bool create) public BlockEntity GetEntity(bool create)
=> GetChunk(create)?.GetBlockEntity(Position.GlobalToChunkRel(), create); => GetChunk(create)?.GetBlockEntity(Position.GlobalToChunkRel(), create);
public void RemoveEntity()
=> GetChunk(false)?.RemoveBlockEntity(Position.GlobalToChunkRel());
public T Get<T>() public T Get<T>()
@ -47,4 +49,8 @@ public class BlockRef
entity.AddChild((Node)(object)value); entity.AddChild((Node)(object)value);
} else throw new ArgumentException($"Unable to access {typeof(T).Name} on a Block", nameof(T)); } else throw new ArgumentException($"Unable to access {typeof(T).Name} on a Block", nameof(T));
} }
// TODO: Clear block entity when last child is removed?
public void Remove<T>() where T : Node
=> GetEntity(false)?.GetNode(typeof(T).Name)?.RemoveFromParent();
} }

@ -49,17 +49,30 @@ public partial class Chunk : Node2D
} }
return layer; return layer;
} }
public void OnLayerChanged(IChunkLayer layer)
=> _dirty = true; public void OnLayerChanged(IChunkLayer layer, BlockPos pos)
{
_dirty = true;
// Clear block entity if block is changed.
if (layer is BlockLayer) RemoveBlockEntity(pos);
}
public BlockEntity GetBlockEntity(BlockPos pos, bool create) public BlockEntity GetBlockEntity(BlockPos pos, bool create)
{ {
EnsureWithinBounds(pos); EnsureWithinBounds(pos);
return create ? this.GetOrCreateChild(pos.ToString(), () => new BlockEntity()) return create ? this.GetOrCreateChild(pos.ToString(), () =>
new BlockEntity(new BlockRef(this.GetWorld(),
pos.ChunkRelToGlobal(ChunkPos))))
: GetNode<BlockEntity>(pos.ToString()); : GetNode<BlockEntity>(pos.ToString());
} }
public void RemoveBlockEntity(BlockPos pos)
{
EnsureWithinBounds(pos);
GetNode(pos.ToString())?.RemoveFromParent();
}
public static void EnsureWithinBounds(BlockPos pos) public static void EnsureWithinBounds(BlockPos pos)
{ {

@ -12,14 +12,14 @@ public interface IChunkLayer : IDeSerializable
{ {
Type AccessType { get; } Type AccessType { get; }
bool IsDefault { get; } bool IsDefault { get; }
event Action<IChunkLayer> Changed; event Action<IChunkLayer, BlockPos> Changed;
} }
public interface IChunkLayer<T> : IChunkLayer public interface IChunkLayer<T> : IChunkLayer
{ {
T this[BlockPos pos] { get; set; } T this[BlockPos pos] { get; set; }
T this[int x, int y] { get; set; } T this[int x, int y] { get; set; }
T this[int index] { get; set; } T this[int index] { get; }
} }
public class ArrayChunkLayer<T> : IChunkLayer<T> public class ArrayChunkLayer<T> : IChunkLayer<T>
@ -32,26 +32,24 @@ public class ArrayChunkLayer<T> : IChunkLayer<T>
public Type AccessType => typeof(T); public Type AccessType => typeof(T);
public bool IsDefault => NonDefaultCount == 0; public bool IsDefault => NonDefaultCount == 0;
public event Action<IChunkLayer> Changed; public event Action<IChunkLayer, BlockPos> Changed;
public T this[BlockPos pos] { public T this[int index] => _data[index];
get => this[Chunk.GetIndex(pos)];
set => this[Chunk.GetIndex(pos)] = value;
}
public T this[int x, int y] { public T this[int x, int y] {
get => this[Chunk.GetIndex(x, y)]; get => this[Chunk.GetIndex(x, y)];
set => this[Chunk.GetIndex(x, y)] = value; set => this[new BlockPos(x, y)] = value;
} }
public T this[int index] { public T this[BlockPos pos] {
get => _data[index]; get => this[Chunk.GetIndex(pos.X, pos.Y)];
set { set {
var index = Chunk.GetIndex(pos.X, pos.Y);
var previous = _data[index]; var previous = _data[index];
if (COMPARER.Equals(value, previous)) return; if (COMPARER.Equals(value, previous)) return;
_data[index] = value; _data[index] = value;
if (!COMPARER.Equals(previous, default)) NonDefaultCount--; if (!COMPARER.Equals(previous, default)) NonDefaultCount--;
if (!COMPARER.Equals(value, default)) NonDefaultCount++; if (!COMPARER.Equals(value, default)) NonDefaultCount++;
Changed?.Invoke(this); Changed?.Invoke(this, pos);
} }
} }
@ -78,10 +76,10 @@ public class TranslationLayer<TData, TAccess> : IChunkLayer<TAccess>
public Type AccessType => typeof(TAccess); public Type AccessType => typeof(TAccess);
public bool IsDefault => _data.IsDefault; public bool IsDefault => _data.IsDefault;
public event Action<IChunkLayer> Changed { add => _data.Changed += value; remove => _data.Changed -= value; } public event Action<IChunkLayer, BlockPos> Changed { add => _data.Changed += value; remove => _data.Changed -= value; }
public TAccess this[BlockPos pos] { get => _from(_data[pos]); set => _data[pos] = _to(value); } public TAccess this[BlockPos pos] { get => _from(_data[pos]); set => _data[pos] = _to(value); }
public TAccess this[int x, int y] { get => _from(_data[x, y]); set => _data[x, y] = _to(value); } public TAccess this[int x, int y] { get => _from(_data[x, y]); set => _data[x, y] = _to(value); }
public TAccess this[int index] { get => _from(_data[index]); set => _data[index] = _to(value); } public TAccess this[int index] => _from(_data[index]);
public void Serialize(ref MessagePackWriter writer, MessagePackSerializerOptions options) public void Serialize(ref MessagePackWriter writer, MessagePackSerializerOptions options)
=> _data.Serialize(ref writer, options); => _data.Serialize(ref writer, options);

@ -97,13 +97,8 @@ public partial class World : Node
} }
[Puppet] [Puppet]
public void SpawnHit(NodePath spritePath, Vector2 hitPosition, Color color) public void SpawnHit(NodePath path, Vector2 hitPosition, Color color)
{ => HitDecal.Spawn(this.GetWorld(), path, hitPosition, color);
var texture = GD.Load<Texture>("res://gfx/hit_decal.png");
var sprite = this.GetWorld().GetNode<Sprite>(spritePath);
var hit = new HitDecal(texture, sprite.Texture, hitPosition, color);
sprite.AddChild(hit);
}
[PuppetSync] [PuppetSync]
public void Despawn(NodePath path, bool errorIfMissing) public void Despawn(NodePath path, bool errorIfMissing)

Loading…
Cancel
Save