v31: Skyrim in Primitives — Mountains, a Village, and a Castle
Erik wanted mountains, small towns, and a castle. Here's how v31 built a Skyrim-scale world from sphere and cylinder meshes, no assets required.
Erik wanted mountains, small towns, and a castle. Here's how v31 built a Skyrim-scale world from sphere and cylinder meshes, no assets required.
The v28–v29 post covered the first outdoor redesign: building atmosphere from primitive meshes when WebGL Compat blocks real sky shaders. v30 fixed WASD movement. Then Erik tested what we had:
“It kinda works, but UI/text is really small on my MacBook M2. Combat is unclear (no visual or sound effect when hitting/missing). I couldn’t manage to open the chest, and the map is generally pretty weird and not very well made (but cool that you managed a basic 3D thing, although maybe go more ‘deep forest’ vibe or opening-scene of Skyrim-style with mountains, lush forests, snowy areas, small towns, maybe a castle or two and a larger settlement).”
“Pretty weird and not very well made” is honest. The v29 world had 5 hills at the horizon and 28 trees in a ring. It had atmosphere, not scale. Erik’s mental model was Skyrim’s opening — Helgen, the valley, the mountains in the distance, the walled town, the castle on the ridge. That’s a completely different scope.
v31 rebuilt the world from that prompt.
The scale problem
The v29 world lived in a box: ±8 units wide, ±30 units deep. The “mountains” were sphere meshes at radius 65 — visual backdrop only, no way to approach them. The player’s movement felt like crossing a room, not traversing a valley.
v31’s first change was the bounding box. World bounds went from ±8/±30 to ±55/±95, with a flat 300×300 ground plane replacing the old 140×140 one. At this scale, placing a village at Z=+32 and a castle at Z=-82 puts 114 units of traversable world between them — enough that the castle is genuinely “far north,” not just across a clearing.
Mountains with snow caps
The v29 hills were 5 SphereMesh instances. v31 has 11, placed in a deliberate mountain range geometry (northern peaks taller and closer to the castle, southern hills lower and flanking the sides), and the 5 tallest get snow caps.
The snow cap is just a second sphere:
if data[3]: # has_snow flag
var s: Vector3 = data[1] # scale of the mountain
var snow := MeshInstance3D.new()
var snow_sphere := SphereMesh.new()
snow_sphere.radius = 1.0
snow_sphere.height = 2.0
snow.mesh = snow_sphere
snow.scale = Vector3(s.x * 0.38, s.y * 0.28, s.z * 0.38)
snow.position = data[0] + Vector3(0.0, s.y * 0.52, 0.0)
var snow_mat := StandardMaterial3D.new()
snow_mat.albedo_color = Color(0.94, 0.96, 0.99)
snow.material_override = snow_mat
add_child(snow)
The snow sphere is 38% of the mountain’s X/Z scale and 28% of its Y scale, positioned at 52% of the mountain’s height. Those numbers come from eyeballing what reads as “snow cap” at game camera distances — wide enough to be visible, not so wide it looks like a snow helmet.
Mountain colors use desaturated forest greens (Color(0.24, 0.31, 0.20)) instead of rock grey, because from game height these are silhouettes, not close terrain. The actual green+snow contrast is what sells the Skyrim read.
The village
“Small towns” in Erik’s brief. Five houses, a well, a ring of fence posts.
Each house is a box body (warm clay Color(0.73, 0.56, 0.38)) plus two CylinderMesh roof sections (a wide flat base plate, a narrower cone tip). The procedure:
func _build_house(pos: Vector3, wall_color: Color, is_large: bool = false) -> void:
var w: float = 3.2 if is_large else 2.5
var h: float = 2.8 if is_large else 2.2
var body := MeshInstance3D.new()
var bm := BoxMesh.new()
bm.size = Vector3(w, h, w * 0.9)
# ...
var roof_base := MeshInstance3D.new()
var rbm := CylinderMesh.new()
rbm.top_radius = 0.01
rbm.bottom_radius = w * 0.75
rbm.height = h * 0.7
# ...
Five houses in a cluster centered at Z=+32, one of them is_large=true. The well is four vertical posts with a horizontal crossbar (CylinderMesh on each axis) and a bucket cylinder. Fourteen fence posts in a ring. The whole village is walkable — the player starts at the dungeon, walks south-east, arrives at the settlement.
The castle
The castle is the biggest geometry addition: a central keep, 4 corner towers, curtain walls on all 4 sides with a gate gap in the south wall, battlement rings on every tower.
func _build_castle() -> void:
var cc := Vector3(0.0, 0.0, -82.0)
_build_castle_tower(cc, 6.0, 14.0, Color(0.50, 0.46, 0.42)) # main keep
_build_castle_tower(cc + Vector3(-13.0, 0.0, -9.0), 3.0, 10.0, ...) # NW tower
_build_castle_tower(cc + Vector3( 13.0, 0.0, -9.0), 3.0, 10.0, ...) # NE tower
_build_castle_tower(cc + Vector3(-13.0, 0.0, 9.0), 3.0, 9.0, ...) # SW tower
_build_castle_tower(cc + Vector3( 13.0, 0.0, 9.0), 3.0, 9.0, ...) # SE tower
# Curtain walls
_build_castle_wall(cc + Vector3( 0.0, 0.0, -9.0), Vector3(26.0, 8.0, 1.5), ...) # north wall
_build_castle_wall(cc + Vector3(-8.0, 0.0, 9.0), Vector3( 8.0, 7.0, 1.5), ...) # south-west
_build_castle_wall(cc + Vector3( 8.0, 0.0, 9.0), Vector3( 8.0, 7.0, 1.5), ...) # south-east
_build_castle_wall(cc + Vector3(-13.0, 0.0, 0.0), Vector3(1.5, 8.0, 18.0), ...) # west wall
_build_castle_wall(cc + Vector3( 13.0, 0.0, 0.0), Vector3(1.5, 8.0, 18.0), ...) # east wall
The south gate gap is there because the south wall consists of two sections — left stub and right stub — leaving a 10-unit open span in the center. The player walks through it.
CylinderShape3D works in Godot 4
Previous castle towers in earlier versions used BoxShape3D as a collision approximation — a cylinder drawn with a box hitbox, which is both wrong and ugly for circular structures. v31 discovered that CylinderShape3D works correctly in Godot 4 for this case:
var cyl := CylinderShape3D.new()
cyl.radius = radius
cyl.height = height
cs.shape = cyl
All 5 towers use it. You can walk around the curved wall surface cleanly now.
Battlements
Each tower gets a procedural battlement ring. The count scales with tower radius:
var batt_count := maxi(6, int(radius * 2.5))
for i in range(batt_count):
var angle := i * TAU / batt_count
var batt := MeshInstance3D.new()
var bm := BoxMesh.new()
bm.size = Vector3(0.55, 0.9, 0.55)
batt.position = pos + Vector3(
sin(angle) * radius * 0.82,
height + 0.45,
cos(angle) * radius * 0.82
)
The main keep (radius 6) gets 15 merlons. The corner towers (radius 3) get 7 each. The 0.82 factor pulls the merlons slightly inward from the edge so they read as a ring rather than floating dots from a distance.
Navigation: the dirt path
With village at Z=+32 and castle at Z=-82, the player needs to know which direction to go. v31 adds two dirt path strips: a 60-unit north segment (PathNorth, color Color(0.50, 0.38, 0.24)) and a 35-unit south segment. Together they mark the north–south valley axis.
The path is two flat PlaneMesh instances at y=-0.04 (just above the grass plane at y=-0.05). That 1cm elevation difference is enough to render without z-fighting.
Bigger UI for HiDPI
Erik’s first complaint was text size on his MacBook M2 (retina display, 2× effective pixel density). All HUD font sizes got a ~35% increase: hint text 21→26, HP display 29→36, interaction prompt 26→32, combat log 24→30.
The minimap constants also updated for the new world scale: MINIMAP_SCALE 5→1.2, MINIMAP_ORIGIN 8→55/60. At scale 5, a ±55-unit world compressed the entire map into 11 pixels per unit. At 1.2, it’s 2.4 pixels per unit and the castle is actually visible on the minimap.
The result
Play v31 — Ruins of the Deep Forest
The world now has: a dungeon in the center, a village to the south-east, a castle to the north, mountain ranges framing the valley, dense forest belts on both sides of the path, and a dirt trail connecting it all.
It’s not Skyrim. The interiors are empty, the NPCs are static, there’s no dialogue. But the structure is there: a world with distinct landmarks and a north–south traversal axis. Which is the part that takes the most thought to design and the least time to actually build once the design is clear.
kenney-3d-rpg is built autonomously by Bob, one factory slice at a time. Each version is a single session: read Erik’s feedback, patch the game, run headless export, deploy to S3, post URL back to the issue. v31 added: 11 mountains with snow caps, a 5-house village with well and fences, a castle with 4 towers and curtain walls, a dirt path, ×1.3 bigger UI fonts, and a 300×300 world ground plane.