I’ve managed to make the Navmesh rebake work. I see the navmesh change upon activating a building.
Problem is that the carving itself doesn’t work. It use to work before but scene hierachy changed. Buildings are added Outside the “Mesh” (Table.obj)
From what I understand, the solution would be to find a way to add the Building entity under the right “NavMesh_Faction” depending on which team it suppose to be a part of.
class_name Buildable
extends Node3D
## signals
## enums
## consts
## exports
@export var build_time: float = 5.0
@export var footprint_size: Vector2i = Vector2i(2, 2) # width (x), depth (z) in grid cells
@export var size_tier: int = 1 # T1 = 1, T2 = 2, ..., T4 = 4
@export var build_cost: int = 50
@export var income_bonus: float = 0
@export var spawn_count: int = 1
## public var
var is_building_started := false
var is_active: bool = false
var owner_builder: BuilderCharacter = null
var is_destroyed := false
var applied_upgrades: Array[Dictionary] = []
## private var
## onready var
@onready var base_spawn_interval:float = get("spawn_interval")
@onready var base_income_bonus:float = get("income_bonus")
@onready var base_defense:float = get("defense")
@onready var base_spawn_count: int = spawn_count
#"obj_" for node references:
## built-in override methods
func _ready() -> void:
add_to_group("obstacle")
pass
func _process(_delta: float) -> void:
pass
## public methods
func get_construction_point(world: World3D) -> Vector3:
var pos := global_position
if has_node("BuildTriggerArea"):
pos = $BuildTriggerArea.global_position
var nav_map := world.navigation_map
return NavigationServer3D.map_get_closest_point(nav_map, pos)
func start_build_process(builder: BuilderCharacter) -> void:
is_building_started = true
owner_builder = builder
remove_from_group("player")
remove_from_group("enemy")
add_to_group(builder.faction)
print("[BUILD START] by", builder.name, "Faction:", builder.faction)
builder.begin_build(self, build_time)
print("Enemy AI building process:", build_time)
func request_build(builder: BuilderCharacter) -> void:
if not is_building_started:
is_building_started = true
owner_builder = builder
remove_from_group("player")
remove_from_group("enemy")
add_to_group(builder.faction)
builder.enqueue_build(self, build_time)
func activate_building() -> void:
is_active = true
add_to_group("targetable")
add_to_group("obstacle")
if owner_builder:
add_to_group(owner_builder.faction)
print("Building activated:", name, "Groups:", get_groups())
var blocker := get_node_or_null("NavBlocker")
if blocker:
var shape := blocker.get_node("CollisionShape3D")
if shape and shape.shape is BoxShape3D:
var box := shape.shape as BoxShape3D
var center := global_position
var radius: float = max(box.size.x, box.size.z) * 0.6
push_entities_out_of_footprint(center, radius)
setup_navmesh_carve(null)
reapply_all_building_carves()
recalculate_stats()
# Override this in child classes
func get_radius() -> float:
return self.radius
func recalculate_stats():
var stats:Dictionary = GameState.final_stats.compute_building(self)
if stats.has("spawn_interval") and get("spawn_interval") != null:
set("spawn_interval", stats["spawn_interval"])
if stats.has("income_bonus") and get("income_bonus") != null:
set("income_bonus", stats["income_bonus"])
if stats.has("defense") and get("defense") != null:
set("defense", stats["defense"])
if stats.has("spawn_count") and get("spawn_count") != null:
set("spawn_count", stats["spawn_count"])
## private methods
func setup_navmesh_carve(nav_region: NavigationRegion3D = null) -> void:
var blocker := get_node_or_null("NavBlocker")
if blocker:
blocker.collision_layer = 1 << 15
blocker.collision_mask = 1 << 15
var shape := blocker.get_node_or_null("CollisionShape3D")
print("Shape:", shape, "has shape:", shape.shape if shape else null)
if shape and shape.shape:
var box := shape.shape as BoxShape3D
var sx := box.size.x * 0.5
var sz := box.size.z * 0.5
var obstacle := get_node_or_null("NavigationObstacle3D")
if obstacle:
obstacle.vertices = PackedVector3Array([
Vector3(-sx, 0, -sz),
Vector3( sx, 0, -sz),
Vector3( sx, 0, sz),
Vector3(-sx, 0, sz),])
obstacle.affect_navigation_mesh = true
obstacle.carve_navigation_mesh = true
print("Set obstacle verts:", obstacle.vertices.size())
if nav_region == null:
var group_name := ""
if is_in_group("player"):
group_name = "player_area"
elif is_in_group("enemy"):
group_name = "enemy_area"
for region in get_tree().get_nodes_in_group(group_name):
if region is NavigationRegion3D:
print("Region: ", region.name , group_name , " polygons: ", region.navigation_mesh.get_polygon_count())
nav_region = region
break
if nav_region:
nav_region.bake_navigation_mesh()
func push_entities_out_of_footprint(center: Vector3, radius: float) -> void:
var space := get_world_3d().direct_space_state
var sphere := SphereShape3D.new()
sphere.radius = radius
var query := PhysicsShapeQueryParameters3D.new()
query.shape = sphere
query.transform = Transform3D(Basis(), center)
query.collision_mask = (1)|(1<<13) # Layer for units/builders
query.collide_with_areas = false
query.collide_with_bodies = true
var results := space.intersect_shape(query, 32)
for result in results:
var body:Node3D = result.collider
if body and (body.is_in_group("units") or body.is_in_group("builder")):
var dir := (body.global_position - center).normalized()
var push_dist := radius + 0.5
var safe_pos := NavigationServer3D.map_get_closest_point(NavigationServer3D.get_maps()[0], center + dir * push_dist)
body.global_position = safe_pos
print("Pushed entity:", body.name)
func reapply_all_building_carves() -> void:
for b in get_tree().get_nodes_in_group("obstacle"):
if b is Buildable and b.is_active:
b.setup_navmesh_carve(null)
func handle_death() -> void:
if is_destroyed:
return
is_destroyed = true
var obstacle := get_node_or_null("NavigationObstacle3D")
if obstacle:
obstacle.affect_navigation_mesh = false
obstacle.carve_navigation_mesh = false
obstacle.visible = false
obstacle.queue_free()
var blocker := get_node_or_null("NavBlocker")
if blocker:
blocker.queue_free()
var shape := get_node_or_null("NavBlocker/CollisionShape3D")
if shape:
shape.disabled = true
await get_tree().physics_frame
await get_tree().physics_frame
if obstacle and obstacle.is_inside_tree():
print("Warning: obstacle still in tree before rebake")
var group_name := ""
if is_in_group("player"):
group_name = "player_area"
elif is_in_group("enemy"):
group_name = "enemy_area"
for region in get_tree().get_nodes_in_group(group_name):
if region is NavigationRegion3D:
region.bake_navigation_mesh()
print("Rebaked navmesh for", group_name, "after removing:", name)
queue_free()
func apply_upgrade(upg: Dictionary) -> void:
var upg_id:String= upg.get("action", "")
for existing in applied_upgrades:
if existing.get("action", "") == upg_id:
print("Upgrade already applied:", upg_id)
return # Skip duplicate
applied_upgrades.append(upg)
print("Registered upgrade:", upg_id)
func get_applied_upgrades() -> Array:
return applied_upgrades
I know my code is horrible, but it is working for the most part (Carving being the not working part)