multiple chunks

This commit is contained in:
2024-08-25 19:23:47 +10:00
parent 8de398ce13
commit 160c9c8a68
6 changed files with 128 additions and 88 deletions

View File

@ -45,6 +45,7 @@ add_executable(Voxelotl MACOSX_BUNDLE
# Game logic classes # Game logic classes
Chunk.swift Chunk.swift
World.swift
Raycast.swift Raycast.swift
Player.swift Player.swift
Game.swift Game.swift
@ -59,6 +60,7 @@ set_source_files_properties(
target_include_directories(Voxelotl PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") target_include_directories(Voxelotl PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
target_link_libraries(Voxelotl PRIVATE SDLSwift) target_link_libraries(Voxelotl PRIVATE SDLSwift)
target_compile_definitions(Voxelotl PRIVATE $<$<CONFIG:Debug>:DEBUG>)
set_target_properties(Voxelotl PROPERTIES set_target_properties(Voxelotl PROPERTIES
XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME YES XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME YES
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "gay.pizza.voxelotl" XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "gay.pizza.voxelotl"

View File

@ -1,21 +1,24 @@
public struct Chunk { public struct Chunk {
public static let chunkSize: Int = 16 public static let shift = 4 // 16
public static let blockCount = chunkSize * chunkSize * chunkSize public static let size: Int = 1 << shift
public static let mask: Int = size - 1
private static let yStride = chunkSize public static let blockCount = size * size * size
private static let zStride = chunkSize * chunkSize
public let position: SIMD3<Int> private static let yStride = size
private static let zStride = size * size
public let origin: SIMD3<Int>
private var blocks: [Block] private var blocks: [Block]
init(position: SIMD3<Int>, blocks: [Block]) { init(position: SIMD3<Int>, blocks: [Block]) {
assert(blocks.count == Self.blockCount) assert(blocks.count == Self.blockCount)
self.position = position self.origin = position
self.blocks = blocks self.blocks = blocks
} }
init(position: SIMD3<Int>) { init(position: SIMD3<Int>) {
self.position = position self.origin = position
self.blocks = Array( self.blocks = Array(
repeating: BlockType.air, repeating: BlockType.air,
count: Self.blockCount count: Self.blockCount
@ -23,9 +26,13 @@ public struct Chunk {
} }
func getBlock(at position: SIMD3<Int>) -> Block { func getBlock(at position: SIMD3<Int>) -> Block {
getBlock(internal: position &- self.origin)
}
func getBlock(internal position: SIMD3<Int>) -> Block {
if position.x < 0 || position.y < 0 || position.z < 0 { if position.x < 0 || position.y < 0 || position.z < 0 {
Block(.air) Block(.air)
} else if position.x >= Self.chunkSize || position.y >= Self.chunkSize || position.z >= Self.chunkSize { } else if position.x >= Self.size || position.y >= Self.size || position.z >= Self.size {
Block(.air) Block(.air)
} else { } else {
blocks[position.x + position.y * Self.yStride + position.z * Self.zStride] blocks[position.x + position.y * Self.yStride + position.z * Self.zStride]
@ -33,10 +40,14 @@ public struct Chunk {
} }
mutating func setBlock(at position: SIMD3<Int>, type: BlockType) { mutating func setBlock(at position: SIMD3<Int>, type: BlockType) {
setBlock(internal: position &- self.origin, type: type)
}
mutating func setBlock(internal position: SIMD3<Int>, type: BlockType) {
if position.x < 0 || position.y < 0 || position.z < 0 { if position.x < 0 || position.y < 0 || position.z < 0 {
return return
} }
if position.x >= Self.chunkSize || position.y >= Self.chunkSize || position.z >= Self.chunkSize { if position.x >= Self.size || position.y >= Self.size || position.z >= Self.size {
return return
} }
@ -44,26 +55,11 @@ public struct Chunk {
} }
mutating func fill(allBy calculation: (_ position: SIMD3<Int>) -> BlockType) { mutating func fill(allBy calculation: (_ position: SIMD3<Int>) -> BlockType) {
var i = 0 for i in 0..<Self.blockCount {
for x in 0..<Self.chunkSize { let x = i & Self.mask
for y in 0..<Self.chunkSize { let y = (i &>> Self.shift) & Self.mask
for z in 0..<Self.chunkSize { let z = (i &>> (Self.shift + Self.shift)) & Self.mask
blocks[i].type = calculation(SIMD3(x, y, z)) blocks[i].type = calculation(self.origin &+ SIMD3(x, y, z))
i += 1
}
}
}
}
func forEach(block perform: (Block, SIMD3<Int>) -> Void) {
for x in 0..<Self.chunkSize {
for y in 0..<Self.chunkSize {
for z in 0..<Self.chunkSize {
let idx = x + y * Self.yStride + z * Self.zStride
let position = SIMD3(x, y, z)
perform(blocks[idx], position)
}
}
} }
} }
@ -75,12 +71,12 @@ public struct Chunk {
var position = SIMD3<Int>() var position = SIMD3<Int>()
for i in self.blocks.indices { for i in self.blocks.indices {
out.append(try transform(blocks[i], position)) out.append(try transform(blocks[i], self.origin &+ position))
position.x += 1 position.x += 1
if position.x == Self.chunkSize { if position.x == Self.size {
position.x = 0 position.x = 0
position.y += 1 position.y += 1
if position.y == Self.chunkSize { if position.y == Self.size {
position.y = 0 position.y = 0
position.z += 1 position.z += 1
} }
@ -98,14 +94,14 @@ public struct Chunk {
var position = SIMD3<Int>() var position = SIMD3<Int>()
for i in self.blocks.indices { for i in self.blocks.indices {
if let element = try transform(blocks[i], position) { if let element = try transform(blocks[i], self.origin &+ position) {
out.append(element) out.append(element)
} }
position.x += 1 position.x += 1
if position.x == Self.chunkSize { if position.x == Self.size {
position.x = 0 position.x = 0
position.y += 1 position.y += 1
if position.y == Self.chunkSize { if position.y == Self.size {
position.y = 0 position.y = 0
position.z += 1 position.z += 1
} }

View File

@ -24,7 +24,7 @@ class Game: GameDelegate {
var camera = Camera(fov: 60, size: .one, range: 0.06...900) var camera = Camera(fov: 60, size: .one, range: 0.06...900)
var player = Player() var player = Player()
var projection: matrix_float4x4 = .identity var projection: matrix_float4x4 = .identity
var chunk = Chunk(position: .zero) var world = World()
func create(_ renderer: Renderer) { func create(_ renderer: Renderer) {
self.resetPlayer() self.resetPlayer()
@ -34,7 +34,7 @@ class Game: GameDelegate {
} }
private func resetPlayer() { private func resetPlayer() {
self.player.position = .init(repeating: 0.5) + .init(0, Float(Chunk.chunkSize), 0) self.player.position = .init(repeating: 0.5) + .init(0, Float(Chunk.size), 0)
self.player.velocity = .zero self.player.velocity = .zero
self.player.rotation = .init(.pi, 0) self.player.rotation = .init(.pi, 0)
} }
@ -50,20 +50,11 @@ class Game: GameDelegate {
UInt64(Arc4Random.instance.next()) | UInt64(Arc4Random.instance.next()) << 32, UInt64(Arc4Random.instance.next()) | UInt64(Arc4Random.instance.next()) << 32,
UInt64(Arc4Random.instance.next()) | UInt64(Arc4Random.instance.next()) << 32)) UInt64(Arc4Random.instance.next()) | UInt64(Arc4Random.instance.next()) << 32))
#endif #endif
let noise = ImprovedPerlin<Float>(random: &random) #if DEBUG
self.chunk.fill(allBy: { position in self.world.generate(width: 2, height: 1, depth: 1, random: &random)
let fpos = SIMD3<Float>(position) #else
return if fpos.y / Float(Chunk.chunkSize) self.world.generate(width: 5, height: 3, depth: 5, random: &random)
+ noise.get(fpos * 0.07) * 0.7 #endif
+ noise.get(fpos * 0.321 + 100) * 0.3 < 0.6 {
.solid(.init(
r: Float16(noise.get(fpos * 0.1)),
g: Float16(noise.get(fpos * 0.1 + 10)),
b: Float16(noise.get(fpos * 0.1 + 100))).mix(.white, 0.4).linear)
} else {
.air
}
})
} }
func fixedUpdate(_ time: GameTime) { func fixedUpdate(_ time: GameTime) {
@ -77,27 +68,24 @@ class Game: GameDelegate {
let deltaTime = min(Float(time.delta.asFloat), 1.0 / 15) let deltaTime = min(Float(time.delta.asFloat), 1.0 / 15)
var destroy = false var reset = false, generate = false
if let pad = GameController.current?.state { if let pad = GameController.current?.state {
if pad.pressed(.back) { reset = true }
if pad.pressed(.start) { generate = true }
}
if Keyboard.pressed(.r) { reset = true }
if Keyboard.pressed(.g) { generate = true }
// Player reset // Player reset
if pad.pressed(.back) { if reset {
self.resetPlayer() self.resetPlayer()
} }
// Regenerate // Regenerate
if pad.pressed(.start) { if generate {
self.generateWorld()
}
}
if Keyboard.pressed(.r) {
self.resetPlayer()
}
if Keyboard.pressed(.g) {
self.generateWorld() self.generateWorld()
} }
self.player.update(deltaTime: deltaTime, chunk: &chunk) self.player.update(deltaTime: deltaTime, world: world)
self.camera.position = player.eyePosition self.camera.position = player.eyePosition
self.camera.rotation = player.eyeRotation self.camera.rotation = player.eyeRotation
} }
@ -114,14 +102,7 @@ class Game: GameDelegate {
specular: Color(rgba8888: 0x2F2F2F00).linear, specular: Color(rgba8888: 0x2F2F2F00).linear,
gloss: 75) gloss: 75)
var instances = chunk.compactMap { block, position in var instances = world.instances
if case let .solid(color) = block.type {
Instance(
position: SIMD3<Float>(chunk.position &+ position) + 0.5,
scale: .init(repeating: 0.5),
color: color)
} else { nil }
}
instances.append( instances.append(
Instance( Instance(
position: player.rayhitPos, position: player.rayhitPos,

View File

@ -8,7 +8,7 @@ struct Player {
to: .init(Self.radius, Self.height, Self.radius)) to: .init(Self.radius, Self.height, Self.radius))
static let eyeLevel: Float = 1.4 static let eyeLevel: Float = 1.4
static let epsilon = Float.ulpOfOne * 10 static let epsilon = Float.ulpOfOne * 20
static let accelerationCoeff: Float = 75 static let accelerationCoeff: Float = 75
static let airAccelCoeff: Float = 3 static let airAccelCoeff: Float = 3
@ -40,7 +40,7 @@ struct Player {
enum JumpInput { case off, press, held } enum JumpInput { case off, press, held }
mutating func update(deltaTime: Float, chunk: inout Chunk) { mutating func update(deltaTime: Float, world: World) {
var turning: SIMD2<Float> = .zero var turning: SIMD2<Float> = .zero
var movement: SIMD2<Float> = .zero var movement: SIMD2<Float> = .zero
var flying: Int = .zero var flying: Int = .zero
@ -133,9 +133,9 @@ struct Player {
self._velocity.y -= Self.gravityCoeff * deltaTime self._velocity.y -= Self.gravityCoeff * deltaTime
// Move & handle collision // Move & handle collision
let checkCorner = { (chunk: Chunk, bounds: AABB, corner: SIMD3<Float>) -> Optional<AABB> in let checkCorner = { (world: World, bounds: AABB, corner: SIMD3<Float>) -> Optional<AABB> in
let blockPos = SIMD3(floor(corner.x), floor(corner.y), floor(corner.z)) let blockPos = SIMD3(floor(corner.x), floor(corner.y), floor(corner.z))
if case BlockType.solid = chunk.getBlock(at: SIMD3<Int>(blockPos)).type { if case BlockType.solid = world.getBlock(at: SIMD3<Int>(blockPos)).type {
let blockGeometry = AABB(from: blockPos, to: blockPos + 1) let blockGeometry = AABB(from: blockPos, to: blockPos + 1)
if bounds.touching(blockGeometry) { if bounds.touching(blockGeometry) {
return blockGeometry return blockGeometry
@ -143,7 +143,7 @@ struct Player {
} }
return nil return nil
} }
let checkCollision = { (chunk: Chunk, position: SIMD3<Float>) -> Optional<AABB> in let checkCollision = { (world: World, position: SIMD3<Float>) -> Optional<AABB> in
let bounds = Self.bounds + position let bounds = Self.bounds + position
let corners: [SIMD3<Float>] = [ let corners: [SIMD3<Float>] = [
.init(bounds.left, bounds.bottom, bounds.far), .init(bounds.left, bounds.bottom, bounds.far),
@ -160,14 +160,14 @@ struct Player {
.init(bounds.right, bounds.top, bounds.near) .init(bounds.right, bounds.top, bounds.near)
] ]
for corner in corners { for corner in corners {
if let geometry = checkCorner(chunk, bounds, corner) { if let geometry = checkCorner(world, bounds, corner) {
return geometry return geometry
} }
} }
return nil return nil
} }
self._position.y += self._velocity.y * deltaTime self._position.y += self._velocity.y * deltaTime
if let aabb = checkCollision(chunk, self._velocity.y > 0 ? self._position + .down * Self.epsilon : self.position) { if let aabb = checkCollision(world, self._velocity.y > 0 ? self._position + .down * Self.epsilon : self.position) {
if self._velocity.y < 0 { if self._velocity.y < 0 {
self._position.y = aabb.top + Self.epsilon self._position.y = aabb.top + Self.epsilon
self._onGround = true self._onGround = true
@ -180,7 +180,7 @@ struct Player {
self._onGround = false self._onGround = false
} }
self._position.x += self._velocity.x * deltaTime self._position.x += self._velocity.x * deltaTime
if let aabb = checkCollision(chunk, self._position) { if let aabb = checkCollision(world, self._position) {
if self._velocity.x < 0 { if self._velocity.x < 0 {
self._position.x = aabb.right + Self.radius + Self.epsilon self._position.x = aabb.right + Self.radius + Self.epsilon
} else { } else {
@ -189,7 +189,7 @@ struct Player {
self._velocity.x = 0 self._velocity.x = 0
} }
self._position.z += self._velocity.z * deltaTime self._position.z += self._velocity.z * deltaTime
if let aabb = checkCollision(chunk, self._position) { if let aabb = checkCollision(world, self._position) {
if self._velocity.z < 0 { if self._velocity.z < 0 {
self._position.z = aabb.near + Self.radius + Self.epsilon self._position.z = aabb.near + Self.radius + Self.epsilon
} else { } else {
@ -200,16 +200,16 @@ struct Player {
// Block picking // Block picking
if let hit = raycast( if let hit = raycast(
chunk: chunk, world: world,
origin: self.eyePosition, origin: self.eyePosition,
direction: .forward * simd_matrix3x3(self.eyeRotation), direction: .forward * simd_matrix3x3(self.eyeRotation),
maxDistance: 3.8 maxDistance: 3.8
) { ) {
self.rayhitPos = hit.position self.rayhitPos = hit.position
if destroy { if destroy {
chunk.setBlock(at: hit.map, type: .air) world.setBlock(at: hit.map, type: .air)
} else if place { } else if place {
chunk.setBlock(at: hit.map.offset(by: hit.side), type: .solid(.white)) world.setBlock(at: hit.map.offset(by: hit.side), type: .solid(.white))
} }
} }

View File

@ -1,7 +1,7 @@
import simd import simd
public func raycast( public func raycast(
chunk: Chunk, world: World,
origin rayPosition: SIMD3<Float>, origin rayPosition: SIMD3<Float>,
direction: SIMD3<Float>, direction: SIMD3<Float>,
maxDistance: Float maxDistance: Float
@ -74,7 +74,7 @@ public func raycast(
} }
// return a result if we hit something solid // return a result if we hit something solid
if chunk.getBlock(at: mapPosition).type != .air { if world.getBlock(at: mapPosition).type != .air {
return .init( return .init(
position: rayPosition + direction * distance, position: rayPosition + direction * distance,
distance: distance, distance: distance,

View File

@ -0,0 +1,61 @@
import Foundation
public class World {
private var _chunks: Dictionary<SIMD3<Int>, Chunk>
public init() {
self._chunks = [:]
}
func getBlock(at position: SIMD3<Int>) -> Block {
return if let chunk = self._chunks[position &>> Chunk.shift] {
chunk.getBlock(at: position)
} else { Block(.air) }
}
func setBlock(at position: SIMD3<Int>, type: BlockType) {
self._chunks[position &>> Chunk.shift]?.setBlock(at: position, type: type)
}
func generate(width: Int, height: Int, depth: Int, random: inout any RandomProvider) {
let noise = ImprovedPerlin<Float>(random: &random)
for x in 0..<width {
for y in 0..<height {
for z in 0..<depth {
let chunkID = SIMD3(x, y, z) &- SIMD3(width, height, depth) / 2
let chunkOrigin = chunkID &<< Chunk.shift
var chunk = Chunk(position: chunkOrigin)
chunk.fill(allBy: { position in
let fpos = SIMD3<Float>(position)
return if fpos.y / Float(Chunk.size)
+ noise.get(fpos * 0.05) * 1.1
+ noise.get(fpos * 0.1 + 500) * 0.5
+ noise.get(fpos * 0.3 + 100) * 0.23 < 0.6 {
.solid(.init(
r: Float16(noise.get(fpos * 0.1)),
g: Float16(noise.get(fpos * 0.1 + 10)),
b: Float16(noise.get(fpos * 0.1 + 100))).mix(.white, 0.6).linear)
} else {
.air
}
})
self._chunks[chunkID] = chunk
}
}
}
}
var instances: [Instance] {
self._chunks.values.flatMap { chunk in
chunk.compactMap { block, position in
if case let .solid(color) = block.type {
Instance(
position: SIMD3<Float>(position) + 0.5,
scale: .init(repeating: 0.5),
color: color)
} else { nil }
}
}
}
}