393 lines
12 KiB
Python
393 lines
12 KiB
Python
|
|
# navigation_demo.py - Visual demo of 3D navigation system
|
||
|
|
# Shows pathfinding and FOV on terrain using VoxelPoint grid
|
||
|
|
# Includes 2D Grid minimap with ColorLayer visualization
|
||
|
|
|
||
|
|
import mcrfpy
|
||
|
|
import sys
|
||
|
|
import math
|
||
|
|
|
||
|
|
# Grid size
|
||
|
|
GRID_W, GRID_H = 40, 40
|
||
|
|
|
||
|
|
# Create demo scene
|
||
|
|
scene = mcrfpy.Scene("navigation_demo")
|
||
|
|
|
||
|
|
# Dark background frame
|
||
|
|
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(15, 15, 25))
|
||
|
|
scene.children.append(bg)
|
||
|
|
|
||
|
|
# Title
|
||
|
|
title = mcrfpy.Caption(text="Navigation System Demo - Pathfinding & FOV", pos=(20, 10))
|
||
|
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||
|
|
scene.children.append(title)
|
||
|
|
|
||
|
|
# Create the 3D viewport (left side, smaller)
|
||
|
|
viewport = mcrfpy.Viewport3D(
|
||
|
|
pos=(20, 50),
|
||
|
|
size=(480, 360),
|
||
|
|
render_resolution=(320, 240),
|
||
|
|
fov=60.0,
|
||
|
|
camera_pos=(20.0, 35.0, 50.0),
|
||
|
|
camera_target=(20.0, 0.0, 20.0),
|
||
|
|
bg_color=mcrfpy.Color(100, 150, 200)
|
||
|
|
)
|
||
|
|
scene.children.append(viewport)
|
||
|
|
|
||
|
|
# Generate terrain using HeightMap
|
||
|
|
print("Generating terrain heightmap...")
|
||
|
|
hm = mcrfpy.HeightMap((GRID_W, GRID_H))
|
||
|
|
hm.mid_point_displacement(0.5, seed=42)
|
||
|
|
hm.normalize(0.0, 1.0)
|
||
|
|
hm.rain_erosion(drops=500, erosion=0.08, sedimentation=0.04, seed=42)
|
||
|
|
hm.normalize(0.0, 1.0)
|
||
|
|
|
||
|
|
# Shift terrain up so most is walkable land instead of water
|
||
|
|
# Add 0.3 offset, then renormalize to 0.1-1.0 range (keeps some water)
|
||
|
|
for y in range(GRID_H):
|
||
|
|
for x in range(GRID_W):
|
||
|
|
hm[x, y] = min(1.0, hm[x, y] + 0.3)
|
||
|
|
|
||
|
|
# Build terrain mesh
|
||
|
|
print("Building terrain mesh...")
|
||
|
|
vertex_count = viewport.build_terrain(
|
||
|
|
layer_name="terrain",
|
||
|
|
heightmap=hm,
|
||
|
|
y_scale=8.0,
|
||
|
|
cell_size=1.0
|
||
|
|
)
|
||
|
|
print(f"Terrain built with {vertex_count} vertices")
|
||
|
|
|
||
|
|
# Create color maps based on height for 3D terrain
|
||
|
|
r_map = mcrfpy.HeightMap((GRID_W, GRID_H))
|
||
|
|
g_map = mcrfpy.HeightMap((GRID_W, GRID_H))
|
||
|
|
b_map = mcrfpy.HeightMap((GRID_W, GRID_H))
|
||
|
|
|
||
|
|
for y in range(GRID_H):
|
||
|
|
for x in range(GRID_W):
|
||
|
|
h = hm[x, y]
|
||
|
|
if h < 0.25: # Water (unwalkable)
|
||
|
|
r_map[x, y] = 0.2
|
||
|
|
g_map[x, y] = 0.4
|
||
|
|
b_map[x, y] = 0.8
|
||
|
|
elif h < 0.6: # Grass (walkable)
|
||
|
|
r_map[x, y] = 0.2 + h * 0.2
|
||
|
|
g_map[x, y] = 0.5 + h * 0.2
|
||
|
|
b_map[x, y] = 0.1
|
||
|
|
elif h < 0.8: # Hills (walkable but costly)
|
||
|
|
r_map[x, y] = 0.5
|
||
|
|
g_map[x, y] = 0.4
|
||
|
|
b_map[x, y] = 0.3
|
||
|
|
else: # Mountains (unwalkable)
|
||
|
|
r_map[x, y] = 0.6
|
||
|
|
g_map[x, y] = 0.6
|
||
|
|
b_map[x, y] = 0.6
|
||
|
|
|
||
|
|
viewport.apply_terrain_colors("terrain", r_map, g_map, b_map)
|
||
|
|
|
||
|
|
# Initialize navigation grid
|
||
|
|
print("Setting up navigation grid...")
|
||
|
|
viewport.grid_size = (GRID_W, GRID_H)
|
||
|
|
viewport.cell_size = 1.0
|
||
|
|
|
||
|
|
# Apply heights from heightmap
|
||
|
|
viewport.apply_heightmap(hm, y_scale=8.0)
|
||
|
|
|
||
|
|
# Mark water as unwalkable (height < 0.25)
|
||
|
|
viewport.apply_threshold(hm, 0.0, 0.25, walkable=False)
|
||
|
|
|
||
|
|
# Mark mountains as unwalkable (height > 0.8)
|
||
|
|
viewport.apply_threshold(hm, 0.8, 1.0, walkable=False)
|
||
|
|
|
||
|
|
# Apply slope costs
|
||
|
|
viewport.set_slope_cost(max_slope=2.0, cost_multiplier=3.0)
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
# Create 2D Grid with ColorLayers (minimap on the right)
|
||
|
|
# ============================================================================
|
||
|
|
print("Creating 2D minimap grid...")
|
||
|
|
|
||
|
|
# Calculate cell size for minimap to fit nicely
|
||
|
|
minimap_width = 320
|
||
|
|
minimap_height = 320
|
||
|
|
cell_px = minimap_width // GRID_W # 8 pixels per cell
|
||
|
|
|
||
|
|
# Create 2D Grid (no texture needed for color layers)
|
||
|
|
grid_2d = mcrfpy.Grid(
|
||
|
|
grid_size=(GRID_W, GRID_H),
|
||
|
|
pos=(520, 50),
|
||
|
|
size=(minimap_width, minimap_height)
|
||
|
|
)
|
||
|
|
scene.children.append(grid_2d)
|
||
|
|
|
||
|
|
# Create and add terrain ColorLayer (z_index=0, bottom layer)
|
||
|
|
terrain_layer = mcrfpy.ColorLayer(z_index=0, name="terrain")
|
||
|
|
grid_2d.add_layer(terrain_layer)
|
||
|
|
|
||
|
|
# Fill terrain layer with colors matching the heightmap
|
||
|
|
for y in range(GRID_H):
|
||
|
|
for x in range(GRID_W):
|
||
|
|
h = hm[x, y]
|
||
|
|
if h < 0.25: # Water
|
||
|
|
terrain_layer.set((x, y), mcrfpy.Color(50, 100, 200))
|
||
|
|
elif h < 0.6: # Grass
|
||
|
|
green = int(100 + h * 100)
|
||
|
|
terrain_layer.set((x, y), mcrfpy.Color(50, green, 30))
|
||
|
|
elif h < 0.8: # Hills
|
||
|
|
terrain_layer.set((x, y), mcrfpy.Color(130, 100, 70))
|
||
|
|
else: # Mountains
|
||
|
|
terrain_layer.set((x, y), mcrfpy.Color(150, 150, 150))
|
||
|
|
|
||
|
|
# Create and add path ColorLayer (z_index=1, on top of terrain)
|
||
|
|
path_layer = mcrfpy.ColorLayer(z_index=1, name="path")
|
||
|
|
grid_2d.add_layer(path_layer)
|
||
|
|
# Initialize transparent
|
||
|
|
path_layer.fill(mcrfpy.Color(0, 0, 0, 0))
|
||
|
|
|
||
|
|
# Create and add FOV ColorLayer (z_index=2, on top of path)
|
||
|
|
fov_layer = mcrfpy.ColorLayer(z_index=2, name="fov")
|
||
|
|
grid_2d.add_layer(fov_layer)
|
||
|
|
# Initialize transparent
|
||
|
|
fov_layer.fill(mcrfpy.Color(0, 0, 0, 0))
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
# State for demo
|
||
|
|
# ============================================================================
|
||
|
|
path_start = [10, 20]
|
||
|
|
path_end = [30, 20]
|
||
|
|
current_path = []
|
||
|
|
fov_visible = []
|
||
|
|
show_fov = [True]
|
||
|
|
show_path = [True]
|
||
|
|
|
||
|
|
def clear_path_layer():
|
||
|
|
"""Clear the path visualization layer"""
|
||
|
|
path_layer.fill(mcrfpy.Color(0, 0, 0, 0))
|
||
|
|
|
||
|
|
def clear_fov_layer():
|
||
|
|
"""Clear the FOV visualization layer"""
|
||
|
|
fov_layer.fill(mcrfpy.Color(0, 0, 0, 0))
|
||
|
|
|
||
|
|
def update_path_visualization():
|
||
|
|
"""Update path layer to show current path"""
|
||
|
|
clear_path_layer()
|
||
|
|
if not show_path[0]:
|
||
|
|
return
|
||
|
|
|
||
|
|
# Draw path in yellow
|
||
|
|
for x, z in current_path:
|
||
|
|
path_layer.set((x, z), mcrfpy.Color(255, 255, 0, 200))
|
||
|
|
|
||
|
|
# Draw start point in green
|
||
|
|
path_layer.set((path_start[0], path_start[1]), mcrfpy.Color(0, 255, 0, 255))
|
||
|
|
|
||
|
|
# Draw end point in red
|
||
|
|
path_layer.set((path_end[0], path_end[1]), mcrfpy.Color(255, 0, 0, 255))
|
||
|
|
|
||
|
|
def update_fov_visualization():
|
||
|
|
"""Update FOV layer to show visible cells"""
|
||
|
|
clear_fov_layer()
|
||
|
|
if not show_fov[0]:
|
||
|
|
return
|
||
|
|
|
||
|
|
# Draw visible cells in semi-transparent blue
|
||
|
|
for x, z in fov_visible:
|
||
|
|
# Don't overwrite start/end markers
|
||
|
|
if [x, z] != path_start and [x, z] != path_end:
|
||
|
|
fov_layer.set((x, z), mcrfpy.Color(100, 200, 255, 80))
|
||
|
|
|
||
|
|
def update_path():
|
||
|
|
"""Recompute path between start and end"""
|
||
|
|
global current_path
|
||
|
|
current_path = viewport.find_path(tuple(path_start), tuple(path_end))
|
||
|
|
print(f"Path: {len(current_path)} steps")
|
||
|
|
update_path_visualization()
|
||
|
|
|
||
|
|
def update_fov():
|
||
|
|
"""Recompute FOV from start position"""
|
||
|
|
global fov_visible
|
||
|
|
fov_visible = viewport.compute_fov(tuple(path_start), radius=8)
|
||
|
|
print(f"FOV: {len(fov_visible)} cells visible")
|
||
|
|
update_fov_visualization()
|
||
|
|
|
||
|
|
# Initial computation
|
||
|
|
update_path()
|
||
|
|
update_fov()
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
# Info panel
|
||
|
|
# ============================================================================
|
||
|
|
info_y = 390
|
||
|
|
|
||
|
|
# Status labels
|
||
|
|
start_label = mcrfpy.Caption(text=f"Start (green): ({path_start[0]}, {path_start[1]})", pos=(520, info_y))
|
||
|
|
start_label.fill_color = mcrfpy.Color(100, 255, 100)
|
||
|
|
scene.children.append(start_label)
|
||
|
|
|
||
|
|
end_label = mcrfpy.Caption(text=f"End (red): ({path_end[0]}, {path_end[1]})", pos=(520, info_y + 22))
|
||
|
|
end_label.fill_color = mcrfpy.Color(255, 100, 100)
|
||
|
|
scene.children.append(end_label)
|
||
|
|
|
||
|
|
path_label = mcrfpy.Caption(text=f"Path (yellow): {len(current_path)} steps", pos=(520, info_y + 44))
|
||
|
|
path_label.fill_color = mcrfpy.Color(255, 255, 100)
|
||
|
|
scene.children.append(path_label)
|
||
|
|
|
||
|
|
fov_label = mcrfpy.Caption(text=f"FOV (blue): {len(fov_visible)} cells", pos=(520, info_y + 66))
|
||
|
|
fov_label.fill_color = mcrfpy.Color(100, 200, 255)
|
||
|
|
scene.children.append(fov_label)
|
||
|
|
|
||
|
|
# Terrain legend
|
||
|
|
legend_y = info_y + 100
|
||
|
|
legend_title = mcrfpy.Caption(text="Terrain Legend:", pos=(520, legend_y))
|
||
|
|
legend_title.fill_color = mcrfpy.Color(200, 200, 200)
|
||
|
|
scene.children.append(legend_title)
|
||
|
|
|
||
|
|
legends = [
|
||
|
|
("Water (blue)", mcrfpy.Color(50, 100, 200), "unwalkable"),
|
||
|
|
("Grass (green)", mcrfpy.Color(80, 150, 30), "walkable"),
|
||
|
|
("Hills (brown)", mcrfpy.Color(130, 100, 70), "costly"),
|
||
|
|
("Mountains (gray)", mcrfpy.Color(150, 150, 150), "unwalkable"),
|
||
|
|
]
|
||
|
|
|
||
|
|
for i, (name, color, desc) in enumerate(legends):
|
||
|
|
cap = mcrfpy.Caption(text=f" {name}: {desc}", pos=(520, legend_y + 22 + i * 20))
|
||
|
|
cap.fill_color = color
|
||
|
|
scene.children.append(cap)
|
||
|
|
|
||
|
|
# Instructions
|
||
|
|
instructions = mcrfpy.Caption(
|
||
|
|
text="[WASD] Move start | [IJKL] Move end | [F] FOV | [P] Path | [Space] Orbit",
|
||
|
|
pos=(20, 430)
|
||
|
|
)
|
||
|
|
instructions.fill_color = mcrfpy.Color(150, 150, 150)
|
||
|
|
scene.children.append(instructions)
|
||
|
|
|
||
|
|
# Status line
|
||
|
|
status = mcrfpy.Caption(text="Status: Navigation demo ready - orbit OFF", pos=(20, 455))
|
||
|
|
status.fill_color = mcrfpy.Color(100, 200, 100)
|
||
|
|
scene.children.append(status)
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
# Animation state - orbit disabled by default
|
||
|
|
# ============================================================================
|
||
|
|
animation_time = [0.0]
|
||
|
|
camera_orbit = [False] # Disabled by default
|
||
|
|
|
||
|
|
def update_display():
|
||
|
|
"""Update info display"""
|
||
|
|
start_label.text = f"Start (green): ({path_start[0]}, {path_start[1]})"
|
||
|
|
end_label.text = f"End (red): ({path_end[0]}, {path_end[1]})"
|
||
|
|
path_label.text = f"Path (yellow): {len(current_path)} steps"
|
||
|
|
fov_label.text = f"FOV (blue): {len(fov_visible)} cells"
|
||
|
|
|
||
|
|
# Camera animation
|
||
|
|
def update_camera(timer, runtime):
|
||
|
|
animation_time[0] += runtime / 1000.0
|
||
|
|
|
||
|
|
if camera_orbit[0]:
|
||
|
|
angle = animation_time[0] * 0.2
|
||
|
|
radius = 35.0
|
||
|
|
center_x = 20.0
|
||
|
|
center_z = 20.0
|
||
|
|
height = 25.0 + math.sin(animation_time[0] * 0.15) * 5.0
|
||
|
|
|
||
|
|
x = center_x + math.cos(angle) * radius
|
||
|
|
z = center_z + math.sin(angle) * radius
|
||
|
|
|
||
|
|
viewport.camera_pos = (x, height, z)
|
||
|
|
viewport.camera_target = (center_x, 2.0, center_z)
|
||
|
|
|
||
|
|
# Key handler
|
||
|
|
def on_key(key, state):
|
||
|
|
if state != mcrfpy.InputState.PRESSED:
|
||
|
|
return
|
||
|
|
|
||
|
|
global current_path, fov_visible
|
||
|
|
|
||
|
|
# Movement for start point (WASD)
|
||
|
|
moved_start = False
|
||
|
|
if key == mcrfpy.Key.W:
|
||
|
|
path_start[1] = max(0, path_start[1] - 1)
|
||
|
|
moved_start = True
|
||
|
|
elif key == mcrfpy.Key.S:
|
||
|
|
path_start[1] = min(GRID_H - 1, path_start[1] + 1)
|
||
|
|
moved_start = True
|
||
|
|
elif key == mcrfpy.Key.A:
|
||
|
|
path_start[0] = max(0, path_start[0] - 1)
|
||
|
|
moved_start = True
|
||
|
|
elif key == mcrfpy.Key.D:
|
||
|
|
path_start[0] = min(GRID_W - 1, path_start[0] + 1)
|
||
|
|
moved_start = True
|
||
|
|
|
||
|
|
# Movement for end point (IJKL)
|
||
|
|
moved_end = False
|
||
|
|
if key == mcrfpy.Key.I:
|
||
|
|
path_end[1] = max(0, path_end[1] - 1)
|
||
|
|
moved_end = True
|
||
|
|
elif key == mcrfpy.Key.K:
|
||
|
|
path_end[1] = min(GRID_H - 1, path_end[1] + 1)
|
||
|
|
moved_end = True
|
||
|
|
elif key == mcrfpy.Key.J:
|
||
|
|
path_end[0] = max(0, path_end[0] - 1)
|
||
|
|
moved_end = True
|
||
|
|
elif key == mcrfpy.Key.L:
|
||
|
|
path_end[0] = min(GRID_W - 1, path_end[0] + 1)
|
||
|
|
moved_end = True
|
||
|
|
|
||
|
|
# Toggle FOV display
|
||
|
|
if key == mcrfpy.Key.F:
|
||
|
|
show_fov[0] = not show_fov[0]
|
||
|
|
update_fov_visualization()
|
||
|
|
status.text = f"FOV display: {'ON' if show_fov[0] else 'OFF'}"
|
||
|
|
|
||
|
|
# Toggle path display
|
||
|
|
if key == mcrfpy.Key.P:
|
||
|
|
show_path[0] = not show_path[0]
|
||
|
|
update_path_visualization()
|
||
|
|
status.text = f"Path display: {'ON' if show_path[0] else 'OFF'}"
|
||
|
|
|
||
|
|
# Toggle camera orbit
|
||
|
|
if key == mcrfpy.Key.SPACE:
|
||
|
|
camera_orbit[0] = not camera_orbit[0]
|
||
|
|
status.text = f"Camera orbit: {'ON' if camera_orbit[0] else 'OFF'}"
|
||
|
|
|
||
|
|
# Quit
|
||
|
|
if key == mcrfpy.Key.ESCAPE:
|
||
|
|
mcrfpy.exit()
|
||
|
|
|
||
|
|
# Update pathfinding and FOV if moved
|
||
|
|
if moved_start or moved_end:
|
||
|
|
update_path()
|
||
|
|
if moved_start:
|
||
|
|
update_fov()
|
||
|
|
update_display()
|
||
|
|
|
||
|
|
# Show cell info
|
||
|
|
vp = viewport.at(path_start[0], path_start[1])
|
||
|
|
status.text = f"Start cell: walkable={vp.walkable}, height={vp.height:.2f}, cost={vp.cost:.2f}"
|
||
|
|
|
||
|
|
# Set up scene
|
||
|
|
scene.on_key = on_key
|
||
|
|
|
||
|
|
# Create timer for camera animation
|
||
|
|
timer = mcrfpy.Timer("camera_update", update_camera, 16)
|
||
|
|
|
||
|
|
# Activate scene
|
||
|
|
mcrfpy.current_scene = scene
|
||
|
|
|
||
|
|
print()
|
||
|
|
print("Navigation Demo loaded!")
|
||
|
|
print(f"A {GRID_W}x{GRID_H} terrain with VoxelPoint navigation grid.")
|
||
|
|
print()
|
||
|
|
print("Left: 3D terrain view")
|
||
|
|
print("Right: 2D minimap with ColorLayer overlays")
|
||
|
|
print(" - Terrain layer shows heightmap colors")
|
||
|
|
print(" - Path layer shows computed A* path (yellow)")
|
||
|
|
print(" - FOV layer shows visible cells (blue tint)")
|
||
|
|
print()
|
||
|
|
print("Controls:")
|
||
|
|
print(" [WASD] Move start point (green)")
|
||
|
|
print(" [IJKL] Move end point (red)")
|
||
|
|
print(" [F] Toggle FOV display")
|
||
|
|
print(" [P] Toggle path display")
|
||
|
|
print(" [Space] Toggle camera orbit")
|
||
|
|
print(" [ESC] Quit")
|