Converting a 2D scene to 3D in Godot: Grand Strategy Devlog #2

Charlie Prince
5 min readMay 29, 2024

--

In my last post, I explained how we created a 2D interactive grand strategy map editor from a vector image in Godot. We started with a uniquely color-coded map, processed it to detect and define regions, states, and countries, and implemented an editor for configuring and saving map data. This provided a solid foundation for our project by focusing on UI and data management, minimizing the need for intricate art or 3D pathfinding, and establishing a basis for a flexible, expandable game engine.

The 2D map editor

This resulted in a reusable method for implementing maps for Grand Strategy games. Before building out the simulation, we need to adapt our approach to facilitate our intended art style. Initially, we considered using a 2D map but lacked the artistic skills to achieve the polished finish we aimed for. We explored other options, such as creating maps in Inkarnate, but these were too stylized. Since the game’s concept is still evolving, we need a flexible starting point.

Ideally, we want an end result similar to the maps in Paradox Grand Strategy games, but we’ll need to add a third dimension to our project first.

Converting the 2D scene to 3D

The first step is to convert our 2D scene to 3D. This is a fairly straightforward task:

  • Replace Node2D with Node3D (for the parent and the Regions nodes).
  • Replace Sprite2D with Sprite3D (the node that holds the region data).
  • Replace Camera2D with Camera3D.

We also need to ensure our scripts are attached and update any references. For instance, instead of extending the Node2D class, we need to update the declaration to extend Node3D. Additionally, we must connect any new node’s signals to our new parent node to listen for events.

Now for the tricky part: we previously created a script that iterates over a color-coded map image and creates a polygon (or multiple) for each region. Here’s our original script for creating the polygons in 2D:

 # Create regions based on pixel colors
for region_color in pixel_color_dict.keys():
var region = load(regionArea).instantiate()
#print(region_color)
if region_color in regions_dict:
region.region_data = regions_dict[region_color]
region.region_data["color"] = region_color
#print(region.region_data)
else:
region.region_data = {
"name": "Unnamed",
"state": "Brittany",
"owner": "None",
"population": 0
}

region.set_name(region.region_data["name"])
region.add_to_group("Persist")
get_node("Regions").add_child(region)

var polygons = get_polygons(image, region_color, pixel_color_dict)

for polygon in polygons:
var region_collision = CollisionPolygon2D.new()
var region_polygon = Polygon2D.new()

region_collision.polygon = polygon
region_polygon.polygon = polygon

# Create the outline using Line2D
var outline = Line2D.new()
outline.width = 2
outline.default_color = Color(0, 0, 0, 1)

# Add points to the outline
for i in range(polygon.size()):
outline.add_point(polygon[i])

# Close the loop
if polygon.size() > 0:
outline.add_point(polygon[0])

region.add_child(outline)

region.add_child(region_collision)
region.add_child(region_polygon)

To render these in 3D, we need to generate a mesh from these polygons. Fortunately, Godot 4 has a handy class CSGPolygon3D which we can use to 'extrude' the polygon to make a 3D shape. It's worth noting that CSGPolygon3D is mainly intended for prototyping and may not be optimal for performance. We'll explore other options in the future, but for testing our map renderer, these will work perfectly. We won't be rendering CSGPolygon3D during gameplay itself, so this shouldn't be too much of an issue.

We also need a way of tracking interactions with the regions, similar to the 2D version. For this, we can use CollisionPolygon3D, which works similarly to CSGPolygon3D in that it takes a 2D polygon and, using the depth property, allows us to extrude it into a 3D collision shape.

Our new script to generate 3D region areas looks like this:

# Create regions based on pixel colors
for region_color in pixel_color_dict.keys():
var region = load(regionArea).instantiate()
if region_color in regions_dict:
region.region_data = regions_dict[region_color]
region.region_data["color"] = region_color
else:
region.region_data = {
"name": "Unnamed",
"state": "Brittany",
"owner": "None",
"population": 0
}

region.set_name(region.region_data["name"])
region.add_to_group("Persist")
get_node("Regions").add_child(region)

var polygons = get_polygons(image, region_color, pixel_color_dict)

for polygon in polygons:
var region_mesh = CSGPolygon3D.new()
var region_collision = CollisionPolygon3D.new()

region_mesh.polygon = polygon
region_mesh.depth = 10
region_collision.set_polygon(polygon)
region.add_child(region_mesh)
region.add_child(region_collision)

Now, in our map editor, we can see our map much the same as before, but now it’s being rendered in 3D space.

3D Rendered map editor

But wait, there’s a problem! Our editor works fine in the 2D scene, but in 3D, none of the collision boxes are working. As it turns out, the 3D scene is rendered behind our UI, which we’ve made to cover the whole viewport. Essentially, we’re just clicking on the UI, not the map. Fortunately, there’s a quick fix for this: set the UI’s mouse filter option to “Ignore.”

Creating Terrain

That’s it for converting our editor to 3D, but it doesn’t look like much yet. Our next step is to create a 3D world map. Since I don’t intend to spend hours learning Blender, the best approach is to generate the terrain from a heightmap. We could do this outside of Godot and then import the world, but I want to keep options open for the future, like procedurally generated maps.

Godot doesn’t have landscaping tools out of the box, so we’ll utilize the HTerrain plugin available on the Godot asset store. We can use the import tool in this plugin to generate a map from a heightmap image. For testing, we created a simple heightmap where the land is white (the tallest part) and the sea is black (the lowest). From this, we’ll generate the basic terrain and refine it later.

Heightmap

Next, we need to generate a second mesh for our oceans. For now, this doesn’t look very pretty but provides a basis for further improvements.

The end result is our interactive map generated in 3D space with collision areas over each region.

Final 3D editor

Finally, our 2D editor has been recreated in 3D! We’re now in a position to start implementing the simulation itself. Follow me on Medium to stay up to date on our progress.

--

--

Charlie Prince

Web Developer based in the UK with a focus on frontend technology and web accessibility