Add ChunkLayer system and world generators

- Potentially multiple layers per chunk
- Add simple and void world generators
- Block is now just a C# class
main
copygirl 4 years ago
parent 3352518f08
commit 1fa14e54d1
  1. 18
      scene/Block.tscn
  2. 2
      scene/GameScene.tscn
  3. 72
      src/Chunk.cs
  4. 111
      src/IO/WorldSave.cs
  5. 14
      src/Items/CreativeBuilding.cs
  6. 9
      src/Network/IntegratedServer.cs
  7. 15
      src/Objects/Block.cs
  8. 2
      src/Scenes/Server.cs
  9. 13
      src/Utility/Extensions.cs
  10. 21
      src/Utility/SceneCache.cs
  11. 107
      src/World.cs
  12. 19
      src/World/Block/Block.cs
  13. 83
      src/World/Chunk/BlockLayer.cs
  14. 38
      src/World/Chunk/Chunk.cs
  15. 113
      src/World/Chunk/ChunkLayer.cs
  16. 41
      src/World/Generation/GeneratorSimple.cs
  17. 23
      src/World/Generation/GeneratorVoid.cs
  18. 25
      src/World/Generation/IWorldGenerator.cs
  19. 133
      src/World/World.cs
  20. 136
      src/World/WorldSave.cs

@ -1,18 +0,0 @@
[gd_scene load_steps=4 format=2]
[ext_resource path="res://gfx/block.png" type="Texture" id=1]
[ext_resource path="res://src/Objects/Block.cs" type="Script" id=2]
[sub_resource type="RectangleShape2D" id=1]
extents = Vector2( 8, 8 )
[node name="Block" type="StaticBody2D"]
collision_layer = 2
collision_mask = 0
script = ExtResource( 2 )
[node name="RectangleShape" type="CollisionShape2D" parent="."]
shape = SubResource( 1 )
[node name="Sprite" type="Sprite" parent="."]
texture = ExtResource( 1 )

@ -1,6 +1,6 @@
[gd_scene load_steps=3 format=2] [gd_scene load_steps=3 format=2]
[ext_resource path="res://src/World.cs" type="Script" id=1] [ext_resource path="res://src/World/World.cs" type="Script" id=1]
[ext_resource path="res://src/Scenes/Game.cs" type="Script" id=3] [ext_resource path="res://src/Scenes/Game.cs" type="Script" id=3]
[node name="Game" type="Node"] [node name="Game" type="Node"]

@ -1,72 +0,0 @@
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");
}
}

@ -1,111 +0,0 @@
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using Godot;
using File = System.IO.File;
public class WorldSave
{
public const string FILE_EXT = ".yf5";
public const int MAGIC_NUMBER = 0x59463573; // "YF5s"
public const int LATEST_VERSION = 1;
public static readonly string WORLDS_DIR = OS.GetUserDataDir() + "/worlds/";
public DateTime LastSaved { get; private set; }
public int Version { get; private set; } = LATEST_VERSION;
public TimeSpan Playtime { get; set; } = TimeSpan.Zero;
public Dictionary<(int, int), Dictionary<BlockPos, (Color, bool)>> Chunks { get; private set; }
public static WorldSave ReadFromFile(string path)
{
var save = new WorldSave { LastSaved = File.GetLastAccessTime(path) };
using (var stream = File.OpenRead(path)) {
using (var reader = new BinaryReader(stream)) {
var magic = reader.ReadInt32();
if (magic != MAGIC_NUMBER) throw new IOException(
$"Magic number does not match ({magic:X8} != {MAGIC_NUMBER:X8})");
// TODO: See how to better support multiple versions, improve saving/loading.
save.Version = reader.ReadUInt16();
save.Playtime = TimeSpan.FromSeconds(reader.ReadUInt32());
if (save.Version == 0) {
save.Chunks = new Dictionary<(int, int), Dictionary<BlockPos, (Color, bool)>>();
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<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;
}
public void WriteToFile(string path)
{
using (var stream = File.OpenWrite(path + ".tmp")) {
using (var writer = new BinaryWriter(stream)) {
writer.Write(MAGIC_NUMBER);
writer.Write((ushort)LATEST_VERSION);
writer.Write((uint)Playtime.TotalSeconds);
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);
}
}
}
}
new Godot.Directory().Rename(path + ".tmp", path);
LastSaved = File.GetLastWriteTime(path);
}
public void WriteDataFromWorld(World world)
=> 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)
{
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);
}
}
}
}

@ -78,7 +78,7 @@ public class CreativeBuilding : Node2D
} }
var world = this.GetWorld(); var world = this.GetWorld();
bool IsBlockAt(BlockPos pos) => world.GetBlockAt(pos) != null; bool IsBlockAt(BlockPos pos) => world.GetBlockDataAt(pos).Block != null;
_canBuild = !IsBlockAt(_startPos) && Facings.All.Any(pos => IsBlockAt(_startPos + pos.ToBlockPos())); _canBuild = !IsBlockAt(_startPos) && Facings.All.Any(pos => IsBlockAt(_startPos + pos.ToBlockPos()));
Update(); // Make sure _Draw is being called. Update(); // Make sure _Draw is being called.
@ -94,7 +94,7 @@ public class CreativeBuilding : Node2D
var world = this.GetWorld(); var world = this.GetWorld();
foreach (var pos in GetBlockPositions(_startPos, _direction, _length)) { foreach (var pos in GetBlockPositions(_startPos, _direction, _length)) {
var hasBlock = world.GetBlockAt(pos) != null; var hasBlock = world.GetBlockDataAt(pos).Block != null;
var color = (_currentMode != BuildMode.Breaking) var color = (_currentMode != BuildMode.Breaking)
? ((_canBuild && !hasBlock) ? green : red) ? ((_canBuild && !hasBlock) ? green : red)
: (hasBlock ? black : red); : (hasBlock ? black : red);
@ -122,10 +122,10 @@ public class CreativeBuilding : Node2D
var start = new BlockPos(x, y); var start = new BlockPos(x, y);
var world = this.GetWorld(); var world = this.GetWorld();
foreach (var pos in GetBlockPositions(start, direction, length)) { foreach (var pos in GetBlockPositions(start, direction, length)) {
if (world.GetBlockAt(pos) != null) continue; if (world.GetBlockDataAt(pos).Block != 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.GetPlayersTracking(pos.ToChunkPos(), true), RPC.Reliable(world.GetPlayersTracking(pos.ToChunkPos(), true),
world.SpawnBlock, pos.X, pos.Y, color, false); world.SetBlockData, pos.X, pos.Y, color.ToRgba32());
} }
} }
@ -142,10 +142,10 @@ public class CreativeBuilding : Node2D
var start = new BlockPos(x, y); var start = new BlockPos(x, y);
var world = this.GetWorld(); var world = this.GetWorld();
foreach (var pos in GetBlockPositions(start, direction, length)) { foreach (var pos in GetBlockPositions(start, direction, length)) {
var block = world.GetBlockAt(pos); var data = world.GetBlockDataAt(pos);
if (block?.Unbreakable != false) continue; if (data.Block == null) continue;
RPC.Reliable(world.GetPlayersTracking(pos.ToChunkPos(), true), RPC.Reliable(world.GetPlayersTracking(pos.ToChunkPos(), true),
world.DespawnBlock, pos.X, pos.Y); world.SetBlockData, pos.X, pos.Y, 0);
} }
} }
} }

@ -12,18 +12,11 @@ public class IntegratedServer : Node
_sceneTree.Root.RenderTargetUpdateMode = Godot.Viewport.UpdateMode.Disabled; _sceneTree.Root.RenderTargetUpdateMode = Godot.Viewport.UpdateMode.Disabled;
// VisualServer.ViewportSetActive(_sceneTree.Root.GetViewportRid(), false); // VisualServer.ViewportSetActive(_sceneTree.Root.GetViewportRid(), false);
var scene = GD.Load<PackedScene>("res://scene/ServerScene.tscn").Init<Server>(); var scene = GD.Load<PackedScene>("res://scene/ServerScene.tscn").Instance<Server>();
_sceneTree.Root.AddChild(scene, true); _sceneTree.Root.AddChild(scene, true);
_sceneTree.CurrentScene = scene; _sceneTree.CurrentScene = scene;
Server = _sceneTree.Root.GetChild<Server>(0); Server = _sceneTree.Root.GetChild<Server>(0);
// Spawn default blocks.
var world = Server.GetWorld();
for (var x = -6; x <= 6; x++) {
var color = Color.FromHsv(GD.Randf(), 0.1F, 1.0F);
world.SpawnBlock(x, 3, color, true);
}
var port = Server.StartSingleplayer(); var port = Server.StartSingleplayer();
this.GetClient().Connect("127.0.0.1", port); this.GetClient().Connect("127.0.0.1", port);
} }

@ -1,15 +0,0 @@
using Godot;
public class Block : StaticBody2D, IInitializable
{
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;
public Sprite Sprite { get; private set; }
public void Initialize() => Sprite = GetNode<Sprite>("Sprite");
}

@ -106,6 +106,6 @@ public class Server : Game
// Local player stays around for reconnecting. // Local player stays around for reconnecting.
if (LocalPlayer == player) return; if (LocalPlayer == player) return;
RPC.Reliable(world.Despawn, world.GetPathTo(player)); RPC.Reliable(world.Despawn, world.GetPathTo(player), true);
} }
} }

@ -5,14 +5,6 @@ using Godot;
public static class Extensions public static class Extensions
{ {
public static T Init<T>(this PackedScene scene)
where T : Node
{
var node = scene.Instance<T>();
(node as IInitializable)?.Initialize();
return node;
}
public static Game GetGame(this Node node) public static Game GetGame(this Node node)
=> node.GetTree().Root.GetChild<Game>(0); => node.GetTree().Root.GetChild<Game>(0);
public static Client GetClient(this Node node) public static Client GetClient(this Node node)
@ -57,8 +49,3 @@ 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; }
} }
public interface IInitializable
{
void Initialize();
}

@ -0,0 +1,21 @@
using System;
using Godot;
public static class SceneCache<T> where T : Node
{
private static readonly PackedScene SCENE
= GD.Load<PackedScene>($"res://scene/{typeof(T).Name}.tscn");
public static T Instance(Action<T> initFunc = null)
{
var node = SCENE.Instance<T>();
(node as IInitializable)?.Initialize();
initFunc?.Invoke(node);
return node;
}
}
public interface IInitializable
{
void Initialize();
}

@ -1,107 +0,0 @@
using System.Collections.Generic;
using Godot;
public class World : Node
{
private static readonly PackedScene BLOCK = GD.Load<PackedScene>("res://scene/Block.tscn");
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");
private static readonly PackedScene HIT_DECAL = GD.Load<PackedScene>("res://scene/HitDecal.tscn");
internal Node PlayerContainer { get; }
internal Node ChunkContainer { get; }
public World()
{
AddChild(PlayerContainer = new Node { Name = "Players" });
AddChild(ChunkContainer = new Node { Name = "Chunks" });
}
public IEnumerable<Player> Players
=> PlayerContainer.GetChildren<Player>();
public Player GetPlayer(int networkID)
=> PlayerContainer.GetNodeOrNull<Player>(networkID.ToString());
public void ClearPlayers()
{ foreach (var player in Players) player.RemoveFromParent(); }
public IEnumerable<Chunk> Chunks
=> ChunkContainer.GetChildren<Chunk>();
public Chunk GetChunkOrNull((int X, int Y) chunkPos)
=> ChunkContainer.GetNodeOrNull<Chunk>($"Chunk ({chunkPos.X}, {chunkPos.Y})");
public Chunk GetOrCreateChunk((int X, int Y) chunkPos)
=> 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]
public void SpawnBlock(int x, int y, Color color, bool unbreakable)
{
var blockPos = new BlockPos(x, y);
var block = BLOCK.Init<Block>();
block.Name = blockPos.ToString();
block.Color = color;
block.Unbreakable = unbreakable;
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]
public void SpawnPlayer(int networkID, Vector2 position)
{
var player = SceneCache<Player>.Instance();
player.NetworkID = networkID;
player.Position = position;
PlayerContainer.AddChild(player);
if (player.IsLocal) {
player.AddChild(new PlayerMovement { Name = "PlayerMovement" });
player.AddChild(new Camera2D { Name = "Camera", Current = true });
this.GetClient().FireLocalPlayerSpawned(player);
}
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]
public void SpawnHit(NodePath spritePath, Vector2 hitPosition, Color color)
{
var hit = HIT_DECAL.Init<HitDecal>();
var sprite = this.GetWorld().GetNode<Sprite>(spritePath);
hit.Add(sprite, hitPosition, color);
}
[PuppetSync]
public void Despawn(NodePath path)
{
var node = GetNode(path);
node.GetParent().RemoveChild(node);
node.QueueFree();
}
}

@ -0,0 +1,19 @@
using Godot;
public class Block
{
public const int LENGTH = 16;
public const int BIT_SHIFT = 4;
public static readonly Block DEFAULT = new Block(
GD.Load<Texture>("res://gfx/block.png"),
new RectangleShape2D { Extents = new Vector2(0.5F, 0.5F) * LENGTH });
public Texture Texture { get; }
public Shape2D Shape { get; }
public Block(Texture texture, Shape2D shape)
{ Texture = texture; Shape = shape; }
}

@ -0,0 +1,83 @@
using System.IO;
using Godot;
public readonly struct BlockData
{
public Block Block { get; } // TODO: Replace with 2-byte or smaller integer identifier?
public int RawColor { get; } // TODO: Replace with smaller representation?
// Perhaps we can fit this into 4 bytes total?
public Color Color => new Color(RawColor);
public BlockData(Block block, int rawColor) { Block = block; RawColor = rawColor; }
public BlockData(Block block, Color color) { Block = block; RawColor = color.ToRgba32(); }
}
public class BlockLayer : BasicChunkLayer<BlockData>
{
private MeshInstance2D _render = null;
private StaticBody2D _collider = null;
public override void _Process(float delta)
{
if (!Dirty) return;
Dirty = false;
var st = (SurfaceTool)null;
if (this.GetGame() is Client) {
if (_render == null) AddChild(_render = new MeshInstance2D
{ Texture = GD.Load<Texture>("res://gfx/block.png") });
st = new SurfaceTool();
st.Begin(Mesh.PrimitiveType.Triangles);
}
_collider?.RemoveFromParent();
if (IsDefault) _collider = null;
else AddChild(_collider = new StaticBody2D());
var size = Block.LENGTH;
var index = 0;
for (var i = 0; i < Chunk.LENGTH * Chunk.LENGTH; i++) {
var data = Data[i];
if (data.Block == null) continue;
var x = (float)(i & Chunk.BIT_MASK);
var y = (float)(i >> Chunk.BIT_SHIFT);
if (_render != null) {
st.AddColor(data.Color);
st.AddUv(new Vector2(0, 0)); st.AddVertex(new Vector3(x - 0.5F, y - 0.5F, 0) * size);
st.AddUv(new Vector2(1, 0)); st.AddVertex(new Vector3(x + 0.5F, y - 0.5F, 0) * size);
st.AddUv(new Vector2(1, 1)); st.AddVertex(new Vector3(x + 0.5F, y + 0.5F, 0) * size);
st.AddUv(new Vector2(0, 1)); st.AddVertex(new Vector3(x - 0.5F, y + 0.5F, 0) * size);
st.AddIndex(index); st.AddIndex(index + 1); st.AddIndex(index + 2);
st.AddIndex(index); st.AddIndex(index + 3); st.AddIndex(index + 2);
index += 4;
}
var ownerID = _collider.CreateShapeOwner(null);
_collider.ShapeOwnerAddShape(ownerID, data.Block.Shape);
_collider.ShapeOwnerSetTransform(ownerID, Transform2D.Identity.Translated(new Vector2(x, y) * size));
}
if (_render != null)
_render.Mesh = st.Commit();
}
public override void Read(BinaryReader reader)
{
NonDefaultCount = 0;
for (var i = 0; i < Chunk.LENGTH * Chunk.LENGTH; i++) {
var color = reader.ReadInt32();
if (color == 0) continue;
Data[i] = new BlockData(Block.DEFAULT, color);
NonDefaultCount++;
}
Dirty = true;
}
public override void Write(BinaryWriter writer)
{
for (var i = 0; i < Chunk.LENGTH * Chunk.LENGTH; i++)
writer.Write(Data[i].RawColor); // Is 0 if block is not set.
}
}

@ -0,0 +1,38 @@
using System.Collections.Generic;
using System.Linq;
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 X, int Y) ChunkPosition { get; }
public IEnumerable<IChunkLayer> Layers
=> GetChildren().OfType<IChunkLayer>();
public Chunk((int X, int Y) chunkPos)
{
Name = $"Chunk ({chunkPos})";
ChunkPosition = chunkPos;
Position = new Vector2(chunkPos.X << (BIT_SHIFT + Block.BIT_SHIFT),
chunkPos.Y << (BIT_SHIFT + Block.BIT_SHIFT));
}
public T GetLayerOrNull<T>() => (T)GetLayerOrNull(typeof(T).Name);
public T GetOrCreateLayer<T>() => (T)GetOrCreateLayer(typeof(T).Name);
public IChunkLayer GetLayerOrNull(string name)
=> GetNodeOrNull<IChunkLayer>(name);
public IChunkLayer GetOrCreateLayer(string name)
{
var layer = GetLayerOrNull(name);
if (layer == null) AddChild((Node)(layer = ChunkLayerRegistry.Create(name)));
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);
}

@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using System.IO;
using Godot;
public interface IChunkLayer
{
bool IsDefault { get; }
void Read(BinaryReader reader);
void Write(BinaryWriter writer);
}
public interface IChunkLayer<T> : IChunkLayer
{
T this[BlockPos pos] { get; set; }
T this[int x, int y] { get; set; }
}
public static class ChunkLayerRegistry
{
private static readonly Dictionary<string, Type> _types = new Dictionary<string, Type>();
static ChunkLayerRegistry()
=> Register<BlockLayer>();
public static void Register<T>()
where T : Node2D, IChunkLayer
=> _types.Add(typeof(T).Name, typeof(T));
public static IChunkLayer Create(string name)
{
var layer = (IChunkLayer)Activator.CreateInstance(_types[name]);
((Node)layer).Name = name;
return layer;
}
}
public static class ChunkLayerExtensions
{
public static void FromBytes(this IChunkLayer layer, byte[] data)
{
using (var stream = new MemoryStream(data))
layer.Read(stream);
}
public static void Read(this IChunkLayer layer, Stream stream)
{
using (var reader = new BinaryReader(stream))
layer.Read(reader);
}
public static byte[] ToBytes(this IChunkLayer layer)
{
using (var stream = new MemoryStream()) {
layer.Write(stream);
return stream.ToArray();
}
}
public static void Write(this IChunkLayer layer, Stream stream)
{
using (var writer = new BinaryWriter(stream))
layer.Write(writer);
}
}
public abstract class BasicChunkLayer<T> : Node2D, IChunkLayer<T>
{
private static readonly IEqualityComparer<T> COMPARER = EqualityComparer<T>.Default;
protected T[] Data { get; } = new T[Chunk.LENGTH * Chunk.LENGTH];
protected bool Dirty { get; set; } = true;
public Chunk Chunk => GetParent<Chunk>();
public int NonDefaultCount { get; protected set; } = 0;
public bool IsDefault => NonDefaultCount == 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 << Chunk.BIT_SHIFT];
}
set {
EnsureWithin(x, y);
var index = x | y << Chunk.BIT_SHIFT;
var previous = Data[index];
if (COMPARER.Equals(value, previous)) return;
if (!COMPARER.Equals(previous, default)) {
if (previous is Node node) RemoveChild(node);
NonDefaultCount--;
}
if (!COMPARER.Equals(value, default)) {
if (value is Node node) AddChild(node);
NonDefaultCount++;
}
Data[index] = value;
Dirty = true;
}
}
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");
}
public abstract void Read(BinaryReader reader);
public abstract void Write(BinaryWriter writer);
}

@ -0,0 +1,41 @@
using System;
using Godot;
public class GeneratorSimple : IWorldGenerator
{
private int _seed;
public string Name => "Simple";
public void SetSeed(int seed) => _seed = seed;
public void Generate(Chunk chunk)
{
if (chunk.ChunkPosition.Y != 0) return;
var rnd = new Random(_seed ^ chunk.ChunkPosition.GetHashCode());
var blocks = chunk.GetOrCreateLayer<BlockLayer>();
for (var x = 0; x < Chunk.LENGTH; x++) {
var grassDepth = rnd.Next(1, 4);
var dirtDepth = rnd.Next(6, 9);
var stoneDepth = rnd.Next(12, 16);
for (var y = 0; y < stoneDepth; y++) {
var color = (y < grassDepth) ? Colors.LawnGreen
: (y < dirtDepth) ? Colors.SaddleBrown
: Colors.Gray;
color = color.Lightened(GD.Randf() * 0.15F);
blocks[x, 3 + y] = new BlockData(Block.DEFAULT, color);
}
}
// TODO: Make it easier to create "structures" that cover multiple chunks.
// TODO: These are supposed to be unbreakable.
if (chunk.ChunkPosition == (-1, 0))
for (var x = Chunk.LENGTH - 6; x < Chunk.LENGTH; x++)
blocks[x, 3] = new BlockData(Block.DEFAULT, Color.FromHsv(GD.Randf(), 0.1F, 1.0F));
else if (chunk.ChunkPosition == (0, 0))
for (var x = 0; x <= 6; x++)
blocks[x, 3] = new BlockData(Block.DEFAULT, Color.FromHsv(GD.Randf(), 0.1F, 1.0F));
}
}

@ -0,0 +1,23 @@
using Godot;
public class GeneratorVoid : IWorldGenerator
{
public string Name => "Void";
public void SetSeed(int seed) { }
public void Generate(Chunk chunk)
{
// TODO: Make it easier to create "structures" that cover multiple chunks.
// TODO: These are supposed to be unbreakable.
if (chunk.ChunkPosition == (-1, 0)) {
var blocks = chunk.GetOrCreateLayer<BlockLayer>();
for (var x = Chunk.LENGTH - 6; x < Chunk.LENGTH; x++)
blocks[x, 3] = new BlockData(Block.DEFAULT, Color.FromHsv(GD.Randf(), 0.1F, 1.0F));
} else if (chunk.ChunkPosition == (0, 0)) {
var blocks = chunk.GetOrCreateLayer<BlockLayer>();
for (var x = 0; x <= 6; x++)
blocks[x, 3] = new BlockData(Block.DEFAULT, Color.FromHsv(GD.Randf(), 0.1F, 1.0F));
}
}
}

@ -0,0 +1,25 @@
using System.Collections.Generic;
public interface IWorldGenerator
{
string Name { get; }
void SetSeed(int seed); // TODO: Allow loading additional generator options.
void Generate(Chunk chunk);
}
public static class WorldGeneratorRegistry
{
private static readonly Dictionary<string, IWorldGenerator> _generators
= new Dictionary<string, IWorldGenerator>();
static WorldGeneratorRegistry()
{
Register(new GeneratorVoid());
Register(new GeneratorSimple());
}
public static void Register(IWorldGenerator generator)
=> _generators.Add(generator.Name, generator);
public static IWorldGenerator GetOrNull(string name)
=> _generators.TryGetValue(name, out var value) ? value : null;
}

@ -0,0 +1,133 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Godot;
public class World : Node
{
internal Node PlayerContainer { get; }
internal Node ChunkContainer { get; }
public IWorldGenerator Generator { get; set; } = WorldGeneratorRegistry.GetOrNull("Simple");
public int Seed { get; set; } = unchecked((int)GD.Randi());
public World()
{
AddChild(PlayerContainer = new Node { Name = "Players" });
AddChild(ChunkContainer = new Node { Name = "Chunks" });
}
public IEnumerable<Player> Players
=> PlayerContainer.GetChildren<Player>();
public Player GetPlayer(int networkID)
=> PlayerContainer.GetNodeOrNull<Player>(networkID.ToString());
public void ClearPlayers()
{ foreach (var player in Players) player.RemoveFromParent(); }
public IEnumerable<Chunk> Chunks
=> ChunkContainer.GetChildren<Chunk>();
public Chunk GetChunkOrNull((int X, int Y) chunkPos)
=> ChunkContainer.GetNodeOrNull<Chunk>($"Chunk ({chunkPos})");
public Chunk GetOrCreateChunk((int X, int Y) chunkPos, bool generate = false)
=> ChunkContainer.GetOrCreateChild($"Chunk ({chunkPos})", () => {
var chunk = new Chunk(chunkPos);
if (generate) Generator.Generate(chunk);
return chunk;
});
[PuppetSync] public void ClearChunks()
{ foreach (var chunk in Chunks) chunk.RemoveFromParent(); }
public BlockData GetBlockDataAt(BlockPos position)
=> GetChunkOrNull(position.ToChunkPos())
?.GetLayerOrNull<BlockLayer>()?[position.GlobalToChunkRel()] ?? default;
[PuppetSync]
public void SetBlockData(int x, int y, int color)
{
var blockPos = new BlockPos(x, y);
GetOrCreateChunk(blockPos.ToChunkPos())
.GetOrCreateLayer<BlockLayer>()[blockPos.GlobalToChunkRel()]
= (color != 0) ? new BlockData(Block.DEFAULT, color) : default;
}
[PuppetSync]
public void SpawnChunk(int chunkX, int chunkY, byte[] data)
{
var chunk = GetOrCreateChunk((chunkX, chunkY));
using (var stream = new MemoryStream(data)) {
using (var reader = new BinaryReader(stream)) {
var numLayers = reader.ReadByte();
for (var i = 0; i < numLayers; i++) {
var name = reader.ReadString();
var layer = chunk.GetOrCreateLayer(name);
layer.Read(reader);
}
}
}
}
private static byte[] ChunkToBytes(Chunk chunk)
{
using (var stream = new MemoryStream()) {
using (var writer = new BinaryWriter(stream)) {
var layers = chunk.GetChildren()
.OfType<IChunkLayer>()
.Where(layer => !layer.IsDefault)
.ToArray();
writer.Write((byte)layers.Length);
foreach (var layer in layers) {
writer.Write(layer.GetType().Name);
layer.Write(writer);
}
}
return stream.ToArray();
}
}
[PuppetSync]
public void SpawnPlayer(int networkID, Vector2 position)
{
var player = SceneCache<Player>.Instance();
player.NetworkID = networkID;
player.Position = position;
PlayerContainer.AddChild(player);
if (player.IsLocal) {
player.AddChild(new PlayerMovement { Name = "PlayerMovement" });
player.AddChild(new Camera2D { Name = "Camera", Current = true });
this.GetClient().FireLocalPlayerSpawned(player);
}
if (this.GetGame() is Server) {
player.VisibilityTracker.ChunkTracked += (chunkPos) => {
var chunk = GetOrCreateChunk(chunkPos, generate: true);
RPC.Reliable(player.NetworkID, SpawnChunk,
chunk.ChunkPosition.X, chunk.ChunkPosition.Y, ChunkToBytes(chunk));
};
player.VisibilityTracker.ChunkUntracked += (chunkPos) => {
var chunk = GetChunkOrNull(chunkPos);
if (chunk == null) return;
RPC.Reliable(player.NetworkID, Despawn, GetPathTo(chunk), false);
};
}
}
[Puppet]
public void SpawnHit(NodePath spritePath, Vector2 hitPosition, Color color)
{
var hit = SceneCache<HitDecal>.Instance();
var sprite = this.GetWorld().GetNode<Sprite>(spritePath);
hit.Add(sprite, hitPosition, color);
}
[PuppetSync]
public void Despawn(NodePath path, bool errorIfMissing)
{
var node = GetNode(path);
if ((node == null) && !errorIfMissing) return;
node.GetParent().RemoveChild(node);
node.QueueFree();
}
}

@ -0,0 +1,136 @@
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using Godot;
using File = System.IO.File;
public class WorldSave
{
public const string FILE_EXT = ".yf5";
public const int MAGIC_NUMBER = 0x59463573; // "YF5s"
public const int LATEST_VERSION = 1;
public static readonly string WORLDS_DIR = OS.GetUserDataDir() + "/worlds/";
public int Version { get; private set; } = LATEST_VERSION;
public TimeSpan Playtime { get; set; } = TimeSpan.Zero;
public DateTime LastSaved { get; private set; }
public string Generator { get; private set; }
public int Seed { get; private set; }
public Dictionary<(int X, int Y), Dictionary<string, byte[]>> ChunkData { get; private set; }
public static WorldSave ReadFromFile(string path)
{
var save = new WorldSave { LastSaved = File.GetLastAccessTime(path) };
using (var stream = File.OpenRead(path)) {
using (var reader = new BinaryReader(stream)) {
var magic = reader.ReadInt32();
if (magic != MAGIC_NUMBER) throw new IOException(
$"Magic number does not match ({magic:X8} != {MAGIC_NUMBER:X8})");
// TODO: See how to better support multiple versions, improve saving/loading.
save.Version = reader.ReadUInt16();
save.Playtime = TimeSpan.FromSeconds(reader.ReadUInt32());
if (save.Version == 0) {
save.Seed = unchecked((int)GD.Randi());
save.Generator = "Void";
var tempBlockLayers = new Dictionary<(int X, int Y), BlockLayer>();
var numBlocks = reader.ReadInt32();
for (var i = 0; i < numBlocks; i++) {
var blockPos = new BlockPos(reader.ReadInt32(), reader.ReadInt32());
var rawColor = reader.ReadInt32();
var unbreakable = reader.ReadBoolean(); // TODO
var chunkPos = blockPos.ToChunkPos();
if (!tempBlockLayers.TryGetValue(chunkPos, out var blocks))
tempBlockLayers.Add(chunkPos, blocks = new BlockLayer());
blocks[blockPos.GlobalToChunkRel()] = new BlockData(Block.DEFAULT, rawColor);
}
save.ChunkData = tempBlockLayers.ToDictionary(kvp => kvp.Key,
kvp => new Dictionary<string, byte[]> { [nameof(BlockLayer)] = kvp.Value.ToBytes() });
} else if (save.Version == 1) {
save.Generator = reader.ReadString();
save.Seed = reader.ReadInt32();
var numChunks = reader.ReadInt32();
save.ChunkData = new Dictionary<(int X, int Y), Dictionary<string, byte[]>>();
for (var i = 0; i < numChunks; i++) {
var chunkPos = (reader.ReadInt32(), reader.ReadInt32());
var chunk = new Dictionary<string, byte[]>();
save.ChunkData.Add(chunkPos, chunk);
var numLayers = reader.ReadByte();
for (var j = 0; j < numLayers; j++) {
var name = reader.ReadString();
var count = reader.ReadInt32();
var data = reader.ReadBytes(count);
chunk.Add(name, data);
}
}
} else throw new IOException($"Version {save.Version} not supported (latest version: {LATEST_VERSION})");
}
}
return save;
}
public void WriteToFile(string path)
{
using (var stream = File.OpenWrite(path + ".tmp")) {
using (var writer = new BinaryWriter(stream)) {
writer.Write(MAGIC_NUMBER);
writer.Write((ushort)LATEST_VERSION);
writer.Write((uint)Playtime.TotalSeconds);
writer.Write(Generator);
writer.Write(Seed);
writer.Write(ChunkData.Count);
foreach (var ((chunkX, chunkY), layers) in ChunkData) {
writer.Write(chunkX);
writer.Write(chunkY);
writer.Write((byte)layers.Count);
foreach (var (name, data) in layers) {
writer.Write(name);
writer.Write(data.Length);
writer.Write(data);
}
}
}
}
new Godot.Directory().Rename(path + ".tmp", path);
LastSaved = File.GetLastWriteTime(path);
}
public void WriteDataFromWorld(World world)
{
Generator = world.Generator.Name;
Seed = world.Seed;
ChunkData = world.Chunks.ToDictionary(
chunk => chunk.ChunkPosition,
chunk => chunk.Layers
.Where(layer => !layer.IsDefault)
.ToDictionary(layer => layer.GetType().Name, layer => layer.ToBytes()));
}
public void ReadDataIntoWorld(World world)
{
world.Generator = WorldGeneratorRegistry.GetOrNull(Generator);
world.Seed = Seed;
RPC.Reliable(world.ClearChunks);
foreach (var (chunkPos, layers) in ChunkData) {
var chunk = world.GetOrCreateChunk(chunkPos);
foreach (var (name, data) in layers)
chunk.GetOrCreateLayer(name).FromBytes(data);
}
}
}
Loading…
Cancel
Save