From 5e40e12c8b3e416f30e602957569a83b13d55c22 Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Tue, 3 Sep 2024 09:18:35 -0400 Subject: [PATCH] implement infinite worlds with threaded chunk generation --- Sources/Voxelotl/CMakeLists.txt | 1 + Sources/Voxelotl/ChunkGeneration.swift | 92 +++++++++++++++++++ .../Common/ConcurrentDictionary.swift | 8 ++ Sources/Voxelotl/Game.swift | 7 +- Sources/Voxelotl/Player.swift | 2 +- Sources/Voxelotl/World.swift | 34 ++++--- Sources/Voxelotl/WorldGenerator.swift | 7 +- 7 files changed, 133 insertions(+), 18 deletions(-) create mode 100644 Sources/Voxelotl/ChunkGeneration.swift diff --git a/Sources/Voxelotl/CMakeLists.txt b/Sources/Voxelotl/CMakeLists.txt index c41295e..1c73f5d 100644 --- a/Sources/Voxelotl/CMakeLists.txt +++ b/Sources/Voxelotl/CMakeLists.txt @@ -53,6 +53,7 @@ add_executable(Voxelotl MACOSX_BUNDLE # Game logic classes Chunk.swift + ChunkGeneration.swift WorldGenerator.swift CubeMeshBuilder.swift ChunkMeshBuilder.swift diff --git a/Sources/Voxelotl/ChunkGeneration.swift b/Sources/Voxelotl/ChunkGeneration.swift new file mode 100644 index 0000000..cb007c2 --- /dev/null +++ b/Sources/Voxelotl/ChunkGeneration.swift @@ -0,0 +1,92 @@ +import Foundation + +public struct ChunkGeneration { + private let queue: OperationQueue + private let localReadyChunks = ConcurrentDictionary, Chunk>() + private var generatingChunkSet = Set>() + + weak var world: World? + + init(queue: DispatchQueue) { + self.queue = OperationQueue() + self.queue.underlyingQueue = queue + self.queue.maxConcurrentOperationCount = 8 + self.queue.qualityOfService = .userInitiated + } + + public mutating func generate(chunkID: SIMD3) { + if !generatingChunkSet.insert(chunkID).inserted { + return + } + + self.queueGenerateJob(chunkID: chunkID) + } + + func queueGenerateJob(chunkID: SIMD3) { + self.queue.addOperation { + guard let world = self.world else { + return + } + let chunk = world.generateSingleChunkUncommitted(chunkID: chunkID) + self.localReadyChunks[chunkID] = chunk + } + } + + public mutating func generateAdjacentIfNeeded(position: SIMD3) { + guard let world = self.world else { + return + } + let centerChunkID = World.makeID(position: position) + for offset in ChunkGeneration.chunkGenerateNeighbors { + let chunkID = centerChunkID &+ offset + if world.getChunk(id: chunkID) == nil { + self.generate(chunkID: chunkID) + } + } + } + + public mutating func acceptReadyChunks() { + guard let world = self.world else { + return + } + + if self.generatingChunkSet.isEmpty { + return + } + + for (chunkID, chunk) in self.localReadyChunks.take() { + world.addChunk(chunkID: chunkID, chunk: chunk) + self.generatingChunkSet.remove(chunkID) + } + } + + private static let chunkGenerateNeighbors: [SIMD3] = [ + SIMD3(-1, -1, -1), + SIMD3(0, -1, -1), + SIMD3(1, -1, -1), + SIMD3(-1, 0, -1), + SIMD3(0, 0, -1), + SIMD3(1, 0, -1), + SIMD3(-1, 1, -1), + SIMD3(0, 1, -1), + SIMD3(1, 1, -1), + SIMD3(-1, -1, 0), + SIMD3(0, -1, 0), + SIMD3(1, -1, 0), + SIMD3(-1, 0, 0), + SIMD3(0, 0, 0), + SIMD3(1, 0, 0), + SIMD3(-1, 1, 0), + SIMD3(0, 1, 0), + SIMD3(1, 1, 0), + SIMD3(-1, -1, 1), + SIMD3(0, -1, 1), + SIMD3(1, -1, 1), + SIMD3(-1, 0, 1), + SIMD3(0, 0, 1), + SIMD3(1, 0, 1), + SIMD3(-1, 1, 1), + SIMD3(0, 1, 1), + SIMD3(1, 1, 1), + ] +} diff --git a/Sources/Voxelotl/Common/ConcurrentDictionary.swift b/Sources/Voxelotl/Common/ConcurrentDictionary.swift index 3a1907e..f61062b 100644 --- a/Sources/Voxelotl/Common/ConcurrentDictionary.swift +++ b/Sources/Voxelotl/Common/ConcurrentDictionary.swift @@ -62,6 +62,14 @@ public class ConcurrentDictionary: Collection { } } + public func take() -> Dictionary { + self.locked { + let current = self.inner + self.inner = [:] + return current + } + } + fileprivate func locked(_ perform: () -> X) -> X { self.lock.lock() defer { diff --git a/Sources/Voxelotl/Game.swift b/Sources/Voxelotl/Game.swift index fbad833..6f0e445 100644 --- a/Sources/Voxelotl/Game.swift +++ b/Sources/Voxelotl/Game.swift @@ -87,8 +87,13 @@ class Game: GameDelegate { // Regenerate current chunk if regenChunk { - self.world.generate(chunkID: World.makeID(position: self.player.position)) + let chunkID = World.makeID(position: self.player.position) + let chunk = self.world.generateSingleChunkUncommitted(chunkID: chunkID) + self.world.addChunk(chunkID: chunkID, chunk: chunk) } + + self.world.generateAdjacentChunksIfNeeded(position: self.player.position) + self.world.update() } func draw(_ renderer: Renderer, _ time: GameTime) { diff --git a/Sources/Voxelotl/Player.swift b/Sources/Voxelotl/Player.swift index 1e064e1..f09e324 100644 --- a/Sources/Voxelotl/Player.swift +++ b/Sources/Voxelotl/Player.swift @@ -8,7 +8,7 @@ struct Player { to: .init(Self.radius, Self.height, Self.radius)) static let eyeLevel: Float = 1.4 - static let epsilon = Float.ulpOfOne * 20 + static let epsilon = Float.ulpOfOne * 2000 static let accelerationCoeff: Float = 75 static let airAccelCoeff: Float = 3 diff --git a/Sources/Voxelotl/World.swift b/Sources/Voxelotl/World.swift index 4ba12a0..c82b61b 100644 --- a/Sources/Voxelotl/World.swift +++ b/Sources/Voxelotl/World.swift @@ -10,11 +10,14 @@ public class World { private var _chunks: Dictionary private var _chunkDamage: Set private var _generator: WorldGenerator + private var _chunkGeneration: ChunkGeneration public init() { self._chunks = [:] self._chunkDamage = [] - self._generator = WorldGenerator() + self._generator = StandardWorldGenerator() + self._chunkGeneration = ChunkGeneration(queue: .global(qos: .userInitiated)) + self._chunkGeneration.world = self } func getBlock(at position: SIMD3) -> Block { @@ -69,29 +72,26 @@ public class World { self._generator.reset(seed: seed) let orig = SIMD3(width, height, depth) / 2 - let localChunks = ConcurrentDictionary() - let queue = OperationQueue() - queue.qualityOfService = .userInitiated for z in 0..) -> Chunk { + self._generator.makeChunk(id: chunkID) + } + + public func generateAdjacentChunksIfNeeded(position: SIMD3) { + self._chunkGeneration.generateAdjacentIfNeeded(position: position) + } + + public func addChunk(chunkID: ChunkID, chunk: Chunk) { + self._chunks[chunkID] = chunk self._chunkDamage.insert(chunkID) for i: ChunkID in [ .X, .Y, .Z ] { for otherID in [ chunkID &- i, chunkID &+ i ] { @@ -102,6 +102,10 @@ public class World { } } + public func update() { + self._chunkGeneration.acceptReadyChunks() + } + func handleRenderDamagedChunks(_ body: (_ id: ChunkID, _ chunk: Chunk) -> Void) { for id in self._chunkDamage { body(id, self._chunks[id]!) diff --git a/Sources/Voxelotl/WorldGenerator.swift b/Sources/Voxelotl/WorldGenerator.swift index fe3b108..312c8b9 100644 --- a/Sources/Voxelotl/WorldGenerator.swift +++ b/Sources/Voxelotl/WorldGenerator.swift @@ -1,4 +1,9 @@ -struct WorldGenerator { +protocol WorldGenerator { + mutating func reset(seed: UInt64) + func makeChunk(id: SIMD3) -> Chunk +} + +struct StandardWorldGenerator: WorldGenerator { var noise: ImprovedPerlin!, noise2: SimplexNoise! public mutating func reset(seed: UInt64) {