Terrain with height-based building restrictions — best approach for a Settlers II-style game?

I’m working on a 2D settlement builder in Godot 4 inspired by The Settlers II. The key mechanic is that terrain height determines what you can build where — flat ground allows large buildings, slopes restrict you to small ones, steep terrain is unbuildable. Height also affects carrier movement speed on roads.

Settlers II achieves this with a staggered vertex grid where each point has a height value and the terrain is rendered as textured triangles between them. Building placement is calculated from height differences between neighboring vertices.

Before I build all of this from scratch, I wanted to ask:

  • Has anyone implemented something similar in Godot — a 2D terrain where tiles/vertices have height values that visually offset them and feed into gameplay logic?

  • Is there an existing addon, plugin, or demo project that handles 2D heightmap terrain? Everything I’ve found (Terrain3D, HTerrain, etc.) is designed for 3D.

  • Would it make more sense to use Godot’s 3D renderer with a locked orthographic camera instead of going pure 2D? That way I’d get height rendering for free, but I’m not sure if that creates more problems than it solves for a game that’s fundamentally 2D in gameplay.

Happy to hear about partial solutions too — even if someone’s just done the rendering side or the data structure side, that would save me from reinventing everything. Thanks!

I love the settlers. But it’s a 3D game, not 2D.

yes, but in 3D. You can’t do this in 2D.
use the meshDataTool to create the geometry. You need an array and functions to access the members using 2 numbers like a matrix.

here’s some old code of mine:

func generate_world() -> void:
	var rng : RandomNumberGenerator = RandomNumberGenerator.new()
	rng.seed = hash("rand")
	#generate mesh
	shared_mesh = ArrayMesh.new()
	shared_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, terrain_mesh.mesh.get_mesh_arrays())
	var mdt : MeshDataTool = MeshDataTool.new()
	mdt.create_from_surface(shared_mesh, 0)
	for i in range(mdt.get_vertex_count()):
		var vert : Vector3 = mdt.get_vertex(i)
		var curr_pos : Vector2i = Vector2i(round(vert.x) + TerrainMap.hmapsiz - 1, round(vert.z) + TerrainMap.hmapsiz - 1)
		var col : Color = TerrainMap.ground_type(curr_pos)
		mdt.set_vertex_color(i, col)
		vert.y = TerrainMap.get_voxel_height(curr_pos)
		mdt.set_vertex(i, vert)
	shared_mesh.clear_surfaces()
	mdt.commit_to_surface(shared_mesh)
	shared_mesh.surface_set_material(0, terrain_material)
	shared_mesh.regen_normal_maps()
	terrain_mesh.mesh = shared_mesh
	terrain_collision.shape.set_faces(shared_mesh.get_faces())
	terrain_mask.queue_redraw()

use 3D, stop overthinking it. 2D cannot handle the high number of sprites required or high quality textures.

There’s a lot of games that are 2D in gameplay but are 3D in graphics, like those top-down 4X games (you can’t really fly up in Stellaris)