Files
godot-demo-projects/3d/voxel/world/voxel_world.gd
2025-10-02 14:35:34 -07:00

138 lines
5.1 KiB
GDScript

class_name VoxelWorld
extends Node
# This file manages the creation and deletion of Chunks.
const CHUNK_MIDPOINT = Vector3(0.5, 0.5, 0.5) * Chunk.CHUNK_SIZE
const CHUNK_END_SIZE = Chunk.CHUNK_SIZE - 1
const DIRECTIONS: Array[Vector3i] = [Vector3i.LEFT, Vector3i.RIGHT, Vector3i.DOWN, Vector3i.UP, Vector3i.FORWARD, Vector3i.BACK]
var render_distance: int:
set(value):
render_distance = value
_delete_distance = value + 2
var _delete_distance := 0
var effective_render_distance := 0
var _old_player_chunk := Vector3i()
var _generating := true
var _deleting := false
var _chunks: Dictionary[Vector3i, Chunk] = {}
@onready var player: CharacterBody3D = $"../Player"
func _process(_delta: float) -> void:
render_distance = Settings.render_distance
var player_chunk := Vector3i((player.transform.origin / Chunk.CHUNK_SIZE).round())
if _deleting or player_chunk != _old_player_chunk:
_delete_far_away_chunks(player_chunk)
_generating = true
if not _generating:
return
# Try to generate chunks ahead of time based on where the player is moving.
@warning_ignore("integer_division")
player_chunk.y += roundi(clampf(player.velocity.y, -render_distance / 4, render_distance / 4))
# Check existing chunks within range. If it doesn't exist, create it.
for x in range(player_chunk.x - effective_render_distance, player_chunk.x + effective_render_distance):
for y in range(player_chunk.y - effective_render_distance, player_chunk.y + effective_render_distance):
for z in range(player_chunk.z - effective_render_distance, player_chunk.z + effective_render_distance):
var chunk_position := Vector3i(x, y, z)
if Vector3(player_chunk).distance_to(Vector3(chunk_position)) > render_distance:
continue
if _chunks.has(chunk_position):
continue
var chunk := Chunk.new(chunk_position)
_chunks[chunk_position] = chunk
add_child(chunk)
chunk.try_initial_generate_mesh(_chunks)
for dir in DIRECTIONS:
var neighbor: Chunk = _chunks.get(chunk_position + dir)
if neighbor != null and not neighbor.is_initial_mesh_generated:
neighbor.try_initial_generate_mesh(_chunks)
# Generate at most one chunk per frame in terms of data/colliders.
# Mesh generation is threaded so it's ok that the above may generate multiple meshes.
return
# If we didn't generate any chunks (and therefore didn't return), what next?
if effective_render_distance < render_distance:
# We can move on to the next stage by increasing the effective distance.
effective_render_distance += 1
else:
# Effective render distance is maxed out, done generating.
_generating = false
func get_block_in_chunk(chunk_position: Vector3i, block_sub_position: Vector3i) -> int:
if _chunks.has(chunk_position):
var chunk: Chunk = _chunks[chunk_position]
if chunk.data.has(block_sub_position):
return chunk.data[block_sub_position]
return 0
func set_block_global_position(block_global_position: Vector3i, block_id: int) -> void:
var chunk_position := Vector3i((Vector3(block_global_position) / Chunk.CHUNK_SIZE).floor())
var chunk: Chunk = _chunks[chunk_position]
var sub_position := Vector3i(Vector3(block_global_position).posmod(Chunk.CHUNK_SIZE))
if block_id == 0:
chunk.data.erase(sub_position)
else:
chunk.data[sub_position] = block_id
chunk.regenerate()
# We also might need to regenerate some neighboring chunks.
if Chunk.is_block_transparent(block_id):
if sub_position.x == 0:
_chunks[chunk_position + Vector3i.LEFT].regenerate()
elif sub_position.x == CHUNK_END_SIZE:
_chunks[chunk_position + Vector3i.RIGHT].regenerate()
if sub_position.z == 0:
_chunks[chunk_position + Vector3i.FORWARD].regenerate()
elif sub_position.z == CHUNK_END_SIZE:
_chunks[chunk_position + Vector3i.BACK].regenerate()
if sub_position.y == 0:
_chunks[chunk_position + Vector3i.DOWN].regenerate()
elif sub_position.y == CHUNK_END_SIZE:
_chunks[chunk_position + Vector3i.UP].regenerate()
func clean_up() -> void:
_chunks = {}
set_process(false)
for c in get_children():
c.free()
func _delete_far_away_chunks(player_chunk: Vector3i) -> void:
_old_player_chunk = player_chunk
# If we need to delete chunks, give the new chunk system a chance to catch up.
effective_render_distance = maxi(1, effective_render_distance - 1)
var deleted_this_frame := 0
# We should delete old chunks more aggressively if moving fast.
# An easy way to calculate this is by using the effective render distance.
# The specific values in this formula are arbitrary and from experimentation.
var max_deletions := clampi(2 * (render_distance - effective_render_distance), 2, 8)
# Also take the opportunity to delete far away chunks.
for chunk_position_key: Vector3i in _chunks.keys():
if Vector3(player_chunk).distance_to(Vector3(chunk_position_key)) > _delete_distance:
_chunks[chunk_position_key].queue_free()
_chunks.erase(chunk_position_key)
deleted_this_frame += 1
# Limit the amount of deletions per frame to avoid lag spikes.
if deleted_this_frame > max_deletions:
# Continue deleting next frame.
_deleting = true
return
# We're done deleting.
_deleting = false