feat: wire infinite chunk streaming into Game with loading screen

This commit is contained in:
2026-06-01 12:57:09 +02:00
parent 4467a7bd10
commit 59466fb291
4 changed files with 76 additions and 77 deletions

View File

@@ -3,76 +3,75 @@ using OpenTK.Mathematics;
using OpenTK.Windowing.Common; using OpenTK.Windowing.Common;
using OpenTK.Windowing.Desktop; using OpenTK.Windowing.Desktop;
using OpenTK.Windowing.GraphicsLibraryFramework; using OpenTK.Windowing.GraphicsLibraryFramework;
using Serilog;
using VoxelEngine.Game;
using VoxelEngine.Rendering; using VoxelEngine.Rendering;
using VoxelEngine.World.Blocks; using VoxelEngine.World.Blocks;
using VoxelEngine.World.Chunk; using VoxelEngine.World.Chunk;
using VoxelEngine.World.Persistence; using VoxelEngine.World.Persistence;
using VoxelEngine.World.Streaming;
using VoxelEngine.World.WorldGen; using VoxelEngine.World.WorldGen;
namespace VoxelEngine.Core; namespace VoxelEngine.Core;
public sealed class Game : GameWindow public sealed class Game : GameWindow
{ {
private const float MoveSpeed = 8f; // units per second private static readonly ILogger Log = Serilog.Log.ForContext<Game>();
private const float Sensitivity = 0.15f; // degrees per pixel
private const int Seed = 1337;
private const bool UseSeed = false;
private readonly Configuration _cfg;
private readonly BlockRegistry _registry = new(); private readonly BlockRegistry _registry = new();
private Shader _shader = null!; private Shader _shader = null!;
private Texture _texture = null!; private Texture _texture = null!;
private readonly Dictionary<Vector3i, ChunkRenderer> _renderer = new(); private LoadingScreen _loading = null!;
private Camera _camera = null!; private World.World _world = null!;
private InMemoryChunkModificationStore _modStore = null!;
private WorldGenerator _generator = null!; private WorldGenerator _generator = null!;
private ChunkStreamer _streamer = null!;
private Player _player = null!;
private World.World _world; public Game(Configuration cfg, GameWindowSettings gameSettings, NativeWindowSettings nativeSettings)
: base(gameSettings, nativeSettings)
public Game(GameWindowSettings gameSettings, NativeWindowSettings nativeSettings) {
: base(gameSettings, nativeSettings) { } this._cfg = cfg;
}
protected override void OnLoad() protected override void OnLoad()
{ {
base.OnLoad(); base.OnLoad();
Log.Information("Game starting with seed={Seed} renderDistanceH={H} renderDistanceV={V}",
this._cfg.Seed, this._cfg.RenderDistanceHorizontal, this._cfg.RenderDistanceVertical);
GL.ClearColor(0.45f, 0.65f, 0.95f, 1.0f); GL.ClearColor(0.45f, 0.65f, 0.95f, 1.0f);
GL.Enable(EnableCap.DepthTest); GL.Enable(EnableCap.DepthTest);
GL.Enable(EnableCap.CullFace); GL.Enable(EnableCap.CullFace);
this._shader = new Shader("Assets/shaders/chunk.vert", "Assets/shaders/chunk.frag"); this._shader = new Shader("Assets/shaders/chunk.vert", "Assets/shaders/chunk.frag");
this._texture = new Texture("Assets/textures/VoxelShadeAtlas.png"); this._texture = new Texture("Assets/textures/VoxelShadeAtlas.png");
this._loading = new LoadingScreen();
this._world = new World.World(this._registry, new InMemoryChunkModificationStore()); this._modStore = new InMemoryChunkModificationStore();
this._world = new World.World(this._registry, this._modStore);
int seed = Game.UseSeed ? Game.Seed : new Random().Next(); int seed = this._cfg.Seed ?? new Random().Next();
this._generator = new WorldGenerator(seed, this._registry); this._generator = new WorldGenerator(seed, this._registry);
for (int cx = -4; cx < 4; cx++) // Spawn at terrain surface above (0, 0).
for (int cz = -4; cz < 4; cz++) int surfaceY = this._generator.SurfaceHeightAt(0, 0);
for (int cy = 0; cy < WorldConstants.WorldHeightChunks; cy++) Vector3 spawnPosition = new(0.5f, surfaceY + 2f, 0.5f);
{
Vector3i pos = new(cx, cy, cz);
this._world.SetChunk(pos, this._generator.Generate(pos));
}
ChunkMesher mesher = new(this._texture.Width, this._texture.Height); this._player = new Player(this._cfg, spawnPosition, this.ClientSize.X / (float)this.ClientSize.Y);
foreach ((Vector3i pos, Chunk chunk) in this._world)
{
Vector3i origin = pos * Chunk.Size;
float[] mesh = mesher.Build(
chunk,
this._registry,
(nx, ny, nz) => this._world.GetBlock(
origin.X + nx,
origin.Y + ny,
origin.Z + nz).IsOpaque
);
this._renderer[pos] = new ChunkRenderer(mesh);
}
this._camera = new Camera( var mesher = new ChunkMesher(this._texture.Width, this._texture.Height);
new Vector3(8f, WorldConstants.SeaLevel + 30f, 40f), this._streamer = new ChunkStreamer(
this.ClientSize.X / (float)this.ClientSize.Y); this._cfg,
this._world,
this._generator.Generate,
mesher,
this._registry,
new ThreadPoolJobExecutor(),
mesh => new ChunkRenderer(mesh));
this.CursorState = CursorState.Grabbed; this.CursorState = CursorState.Grabbed;
} }
@@ -82,24 +81,22 @@ public sealed class Game : GameWindow
base.OnUpdateFrame(args); base.OnUpdateFrame(args);
if (!this.IsFocused) return; if (!this.IsFocused) return;
if (this.KeyboardState.IsKeyDown(Keys.Escape)) { if (this.KeyboardState.IsKeyDown(Keys.Escape)) { this.Close(); return; }
this.Close(); return;
}
float speedMulti = 1f; float speedMulti = this.KeyboardState.IsKeyDown(Keys.LeftShift) ? 1.8f : 1f;
float distance = this._player.MoveSpeed * speedMulti * (float)args.Time;
if (this.KeyboardState.IsKeyDown(Keys.LeftShift)) speedMulti = 1.8f ;
float distance = Game.MoveSpeed * speedMulti * (float)args.Time; if (this.KeyboardState.IsKeyDown(Keys.W)) this._player.Move(this._player.Camera.Front * distance);
if (this.KeyboardState.IsKeyDown(Keys.W)) this._camera.Move(this._camera.Front * distance); if (this.KeyboardState.IsKeyDown(Keys.S)) this._player.Move(-this._player.Camera.Front * distance);
if (this.KeyboardState.IsKeyDown(Keys.S)) this._camera.Move(-this._camera.Front * distance); if (this.KeyboardState.IsKeyDown(Keys.A)) this._player.Move(-this._player.Camera.Right * distance);
if (this.KeyboardState.IsKeyDown(Keys.A)) this._camera.Move(-this._camera.Right * distance); if (this.KeyboardState.IsKeyDown(Keys.D)) this._player.Move(this._player.Camera.Right * distance);
if (this.KeyboardState.IsKeyDown(Keys.D)) this._camera.Move(this._camera.Right * distance); if (this.KeyboardState.IsKeyDown(Keys.Space)) this._player.Move(Vector3.UnitY * distance);
if (this.KeyboardState.IsKeyDown(Keys.Space)) this._camera.Move(Vector3.UnitY * distance); if (this.KeyboardState.IsKeyDown(Keys.LeftControl)) this._player.Move(-Vector3.UnitY * distance);
if (this.KeyboardState.IsKeyDown(Keys.LeftControl)) this._camera.Move(-Vector3.UnitY * distance);
Vector2 delta = this.MouseState.Delta; Vector2 delta = this.MouseState.Delta;
this._camera.AddRotation(delta.X * Game.Sensitivity, -delta.Y * Game.Sensitivity); this._player.AddRotation(delta.X * this._player.MouseSensitivity, -delta.Y * this._player.MouseSensitivity);
this._streamer.Tick(this._player.ChunkPosition);
} }
protected override void OnRenderFrame(FrameEventArgs args) protected override void OnRenderFrame(FrameEventArgs args)
@@ -108,21 +105,25 @@ public sealed class Game : GameWindow
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
this._shader.Use(); if (!this._streamer.SpawnReady)
this._shader.SetMatrix4("view", this._camera.GetViewMatrix()); {
this._shader.SetMatrix4("projection", this._camera.GetProjectionMatrix()); this._loading.Draw();
this._shader.SetInt("atlas", 0); this.SwapBuffers();
this._texture.Use(); return;
}
foreach ((Vector3i pos, ChunkRenderer renderer) in this._renderer) this._shader.Use();
this._shader.SetMatrix4("view", this._player.Camera.GetViewMatrix());
this._shader.SetMatrix4("projection", this._player.Camera.GetProjectionMatrix());
this._shader.SetInt("atlas", 0);
this._texture.Use(TextureUnit.Texture0);
this._streamer.DrawAll((pos, renderer) =>
{ {
this._shader.SetMatrix4("model", Matrix4.CreateTranslation( this._shader.SetMatrix4("model", Matrix4.CreateTranslation(
pos.X * Chunk.Size, pos.X * Chunk.Size, pos.Y * Chunk.Size, pos.Z * Chunk.Size));
pos.Y * Chunk.Size,
pos.Z * Chunk.Size
));
renderer.Draw(); renderer.Draw();
} });
this.SwapBuffers(); this.SwapBuffers();
} }
@@ -131,18 +132,16 @@ public sealed class Game : GameWindow
{ {
base.OnResize(e); base.OnResize(e);
GL.Viewport(0, 0, e.Width, e.Height); GL.Viewport(0, 0, e.Width, e.Height);
// OnResize can fire before OnLoad (where _camera is created), so guard against it. if (this._player is not null) this._player.Camera.AspectRatio = e.Width / (float)e.Height;
if (this._camera is not null) this._camera.AspectRatio = e.Width / (float)e.Height;
} }
protected override void OnUnload() protected override void OnUnload()
{ {
foreach (ChunkRenderer rendererValue in this._renderer.Values) Log.Information("Game shutting down");
{ this._streamer?.Dispose();
rendererValue.Dispose(); this._loading?.Dispose();
} this._texture?.Dispose();
this._texture.Dispose(); this._shader?.Dispose();
this._shader.Dispose();
base.OnUnload(); base.OnUnload();
} }
} }

View File

@@ -20,16 +20,18 @@ Log.Logger = new LoggerConfiguration()
try try
{ {
var cfg = new Configuration();
NativeWindowSettings nativeSettings = new() NativeWindowSettings nativeSettings = new()
{ {
ClientSize = new Vector2i(1280, 720), ClientSize = new Vector2i(cfg.WindowWidth, cfg.WindowHeight),
Title = "VoxelEngine", Title = "VoxelEngine",
APIVersion = new Version(3, 3), APIVersion = new Version(3, 3),
Profile = ContextProfile.Core, Profile = ContextProfile.Core,
Flags = ContextFlags.ForwardCompatible Flags = ContextFlags.ForwardCompatible
}; };
using Game game = new(GameWindowSettings.Default, nativeSettings); using Game game = new(cfg, GameWindowSettings.Default, nativeSettings);
game.Run(); game.Run();
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -1,4 +1,3 @@
using System.Collections;
using OpenTK.Mathematics; using OpenTK.Mathematics;
using VoxelEngine.Core; using VoxelEngine.Core;
using VoxelEngine.World.Blocks; using VoxelEngine.World.Blocks;
@@ -7,7 +6,7 @@ using VoxelEngine.World.Persistence;
namespace VoxelEngine.World; namespace VoxelEngine.World;
public sealed class World(BlockRegistry registry, IChunkModificationStore modStore) public sealed class World(BlockRegistry registry, IChunkModificationStore modStore)
: IWorld, IEnumerable<KeyValuePair<Vector3i, Chunk.Chunk>> : IWorld
{ {
private readonly Dictionary<Vector3i, Chunk.Chunk> _chunks = new(); private readonly Dictionary<Vector3i, Chunk.Chunk> _chunks = new();
@@ -60,7 +59,4 @@ public sealed class World(BlockRegistry registry, IChunkModificationStore modSto
return true; return true;
} }
public IEnumerator<KeyValuePair<Vector3i, Chunk.Chunk>> GetEnumerator() => this._chunks.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
} }

View File

@@ -23,6 +23,8 @@ public sealed class WorldGenerator
this._terrain = new TerrainSampler(noise, this._biomeMap); this._terrain = new TerrainSampler(noise, this._biomeMap);
} }
public int SurfaceHeightAt(int worldX, int worldZ) => this._terrain.HeightAt(worldX, worldZ);
public VoxelChunk Generate(Vector3i chunkPos) public VoxelChunk Generate(Vector3i chunkPos)
{ {
VoxelChunk chunk = new(); VoxelChunk chunk = new();