Quake Mapper

by ELF32bit

10

Quake mapping plugin for Godot 4

Preview
Mapper plugin provides a way to manage game directories with map resources.

Construct Godot scenes from maps using your own scripts and run them without the plugin.

Organize map resources into game expansions by specifying alternative game directories.

Available in Godot Asset Library

Comprehensive Quake game profile for this plugin

Additional tools for creating maps are available here

Features

  • Progressive loading of complex maps as scenes in a deterministic way.
  • Automatic loading of PBR textures, animated textures and shader material textures.
  • Effortless brush entity construction and animation using plugin functions.
  • Safe entity property parsing and binding, entity linking and grouping.
  • Ability to scatter grass on textures and barycentric wireframes!
  • Texture WAD (WAD2, WAD3) and Palette support.
  • Basic MDL (IDPO version 6) support.

Usage

1. Create game directory with map resources.

  • game/builders for entity build scripts.
  • game/materials for override materials with additional metadata.
  • game/textures for textures with possible PBR or animation names.
  • game/sounds for loading sounds with any of the supported extensions.
  • game/maps for maps, also maps might embed each other in entity properties.
  • game/mapdata for automatically generated lightmaps and navigation data.
  • game/wads for texture WADs defined in map files.
  • game/mdls for animated models.

2. Construct map entities with build scripts.

Scripts inside builders directory are used to construct map entities.

Entity classname property determines which build script the plugin will execute.

Build scripts ending with underscore can be used to construct many similar entities.

For example, trigger_.gd will be executed for trigger_once and trigger_multiple entities.

MapperUtilities class provides smart build functions.

# func_breakable.gd will create individual brushes
static func build(map: MapperMap, entity: MapperEntity) -> Node:
	return MapperUtilities.create_brush_entity(entity, "Node3D", "RigidBody3D")
# worldspawn.gd brushes will be merged into a single geometry
static func build(map: MapperMap, entity: MapperEntity) -> Node:
	return MapperUtilities.create_merged_brush_entity(entity, "StaticBody3D")
# trigger_multiple.gd will create Area3D with a single merged collision shape
static func build(map: MapperMap, entity: MapperEntity) -> Node:
	return MapperUtilities.create_merged_brush_entity(entity, "Area3D",
		false, true, false)
# func_decal.gd will create an improvised decal from a brush
static func build(map: MapperMap, entity: MapperEntity) -> Node:
	return MapperUtilities.create_decal_entity(entity)

Create entity node or nodes, set a script with @export annotations and bind entity properties.

# info_player_start.gd
static func build(map: MapperMap, entity: MapperEntity) -> Node:
	var entity_node := Marker3D.new()
	entity_node.add_to_group("info_player_start", true)
	return entity_node # origin and angles are assigned automatically
# light_.gd
static func build(map: MapperMap, entity: MapperEntity) -> Node:
	var entity_node := OmniLight3D.new()
	entity_node.set_script(preload("../scripts/light.gd"))
	entity_node.omni_range = entity.get_unit_property("light", 300)
	entity_node.light_color = entity.get_color_property("_color", Color.WHITE)
	return entity_node # origin and angles are assigned automatically

Entity linking information is also avaliable, but linked entities might not be constructed yet.

# light_.gd
var entity_target := map.get_first_entity_target(entity,
	"target", "targetname", "info_null")
if entity_target:
	entity_target.get_origin_property(null) # is available
	entity_target.node # is most likely missing
	entity_target.center # stores AABB center

Post build script named __post.gd can be executed after all entity nodes are constructed.

3. Define map override materials with additional metadata.

Materials support the same naming pattern with underscore as build scripts.

Moreover, material named WOOD_.tres will also apply to WOOD1, WOOD2, etc.

Shader materials that use standard texture parameters will be assigned provided textures.

For example, albedo_texture or normal_texture uniforms inside a shader.

Material metadata can affect how nodes of uniform brushes are generated.

  • mesh_disabled set to true will hide MeshInstance3D.

Meshes of uniform brushes will not be merged into entity mesh.

  • cast_shadow set to false will disable shadow casting on MeshInstance3D.

Meshes of uniform brushes will be excluded from entity shadow mesh.

  • gi_mode will set MeshInstance3D gi_mode to the specified mode.
  • ignore_occlusion will disable occlusion culling for MeshInstance3D.
  • collision_disabled set to true will disable CollisionShape3D.

Shapes of uniform brushes will not be merged into entity collision shape.

  • collision_layer will set CollisionObject3D layer to the specified layer.
  • collision_mask will set CollisionObject3D mask to the specified mask.
  • occluder_disabled set to true will hide OccluderInstance3D.

Occluders of uniform brushes will not be merged into entity occluder.

  • occluder_mask will set OccluderInstance3D mask to the specified mask.

Material metadata can be used to filter out special brushes.

# WATER_.tres material
metadata/liquid = 1
metadata/mesh_disabled = true
metadata/collision_disabled = true
metadata/occluder_disabled = true

# worldspawn.gd (created as the merged brush entity)
for brush in entity.brushes:
	if not brush.get_uniform_property("liquid", 0) > 0:
		continue
	# liquid area is created at a global position
	var liquid_area := MapperUtilities.create_brush(entity, brush, "Area3D")
	if not liquid_area:
		continue
	# manually re-enabling disabled brush nodes
	for child in liquid_area.get_children():
		if child is MeshInstance3D:
			child.visible = true
		elif child is CollisionShape3D:
			child.disabled = false
		elif child is OccluderInstance3D:
			child.visible = true
	# parenting one global node to another with right local coordinates
	MapperUtilities.add_global_child(liquid_area, entity_node, map.settings)

4. Animated textures and material alternative textures.

Generic textures are using complex naming pattern.

Animated texture with 3 frames, possibly followed by PBR suffix.

  • texture-0.png
  • texture-1_albedo.png
  • texture-2.png

AnimatedTexture resource can be created alongside for more control.

Material alternative textures, possibly followed by animated texture suffix.

  • texture+0.png
  • texture+1-0.png
  • texture+1-1_albedo.png
  • texture+1-2.png
  • texture+2_albedo.png

Quake textures with similar prefixes (+0, +1, +a, +b) can also be loaded.

Plugin supports multiple loading schemes for various resources.

Custom loader can be implemented for your own assets.

5. Bind entity properties to node properties.

Simple entity properties can be bound to entity node properties.

Assignment of node properties happens after entity node is constructed.

Common entity properties such as origin, angle, angles (mangle) are already bound.

entity.bind_int_property("hp", "health")

Sometimes it's necessary to modify entity properties before assigning.

entity_node.set_script(preload("../scripts/monster_dog.gd"))
entity_node.health = maxi(entity.get_int_property("hp", 100), 10)

Complex entity properties such as signals and node paths can also be bound.

For example, trigger might need to send activation signals to nodes of other entities.

entity_node.set_script(preload("../scripts/trigger.gd")) # with "generic" signal
entity.bind_signal_property("target", "targetname", "generic", "_on_generic_signal")
entity.bind_signal_property("killtarget", "targetname", "generic", "queue_free")
entity_node.set_script(preload("../scripts/path_corner.gd")) # with "targets" export
entity.bind_node_path_array_property("target", "targetname", "targets", "path_corner")

Other entity properties can be manually inserted into entity node properties.

entity.node_properties["my_property"] = value

Changing automatically assigned properties will adjust the pivot of an entity.

# func_turnable.gd
var pivot_offset := Vector3.DOWN * entity.aabb.size.y / 2.0
entity.node_properties["position"] = entity.center + pivot_offset
return MapperUtilities.create_merged_brush_entity(entity, "StaticBody3D")

6. Assign navigation regions.

Various entities might affect navigation regions differently.

Use entity node groups to manage entity navigation groups.

# worldspawn.gd
var navigation_region := MapperUtilities.create_navigation_region(map, entity_node)
MapperUtilities.add_to_navigation_region(entity_node, navigation_region)

# func_detail entities will affect worldspawn navigation region
for map_entity in map.classnames.get("func_detail", []):
	# same as map_entity.node_groups.append("my_entity_group_name")
	MapperUtilities.add_entity_to_navigation_region(map_entity, navigation_region)

Assignment of node groups happens after entity node is constructed.

7. Generate surface and volume distributions.

Distributions are stored as transform arrays with basis and origin.

MapperUtilities class provides functions for working with transform arrays.

Spread function filters out nearby points, rotation function takes snap angles.

# worldspawn.gd
var grass_multimesh := preload("../resources/multimesh.tres")
var grass_transform_array := entity.generate_surface_distribution(
	["GRASS*", "__TB_empty"], 1.0, 0.0, 60.0, false, false, 0)

MapperUtilities.spread_transform_array(grass_transform_array, 0.25)
MapperUtilities.scale_transform_array(grass_transform_array,
	Vector3(1.0, 1.0, 1.0), Vector3(1.5, 2.0, 1.5))
MapperUtilities.rotate_transform_array(grass_transform_array,
	Vector3(-1.0, 0.0, -1.0)) # rotates around up vector without snap

An example of using point entities to erase grass.

# obtaining entity node global transform without scene tree
var transform := MapperUtilities.get_tree_transform(entity_node)
var inverse_transform := transform.affine_inverse()

for map_entity in map.classnames.get("info_eraser", []):
	var position = map_entity.get_origin_property(null)
	if position == null:
		continue
	# same as entity_node.to_local(position)
	var local_position := inverse_transform * Vector3(position)
	var radius = map_entity.get_unit_property("radius", 300.0)
	var hardness = map_entity.get_float_property("hardness", 1.0)
	MapperUtilities.erase_transform_array(
		grass_transform_array, local_position, radius, hardness)

var grass_multimesh_instance := MapperUtilities.create_multimesh_instance(
	entity, entity_node, grass_multimesh, grass_transform_array)

Distributions can be configured to generate world space data for placing other nodes.

var transform_array := entity.generate_volume_distribution(
	1.0, 0.5, INF, Basis.IDENTITY, true, 0)
var positions := MapperUtilities.get_transform_array_positions(
	transform_array, Vector3(0.0, 1.0, 0.0)) # up offset

8. Load map resources using game loader.

Game loader can retrieve resources without extensions from multiple game directories.

# resources require full game directory path
var noise1 := map.loader.load_sound("sounds/ambience/swamp1")
var noise2 := map.loader.load_sound("sounds/ambience/swamp2")
var supershotgun := map.loader.load_mdl_raw("mdls/items/g_shot")

Sub-maps are constructed using settings from the main map and stored inside a cache.

Instances of cached sub-maps will have unusable overlapping navigation regions.

# misc_explobox.gd will construct sub-map or retrieve it from cache
var explobox := map.loader.load_map_raw("maps/items/b_explob", true)
if explobox:
	var explobox_instance := explobox.instantiate()
	return explobox_instance

Caching can be disabled for loading sub-maps with unique navigation regions.

# __post.gd
for index in range(100):
	# island prefab will be constructed 100 times with unique navigation regions
	var island_prefab := map.loader.load_map_raw("maps/islands/variant1", false)
	if not island_prefab:
		continue
	# spawning 100 islands with random positions
	var island_prefab_instance := island_prefab.instantiate()
	map.node.add_child(island_prefab_instance, map.settings.readable_node_names)
	island_prefab_instance.position += Vector3(randf(), 0.0, randf()) * 1000.0

Maps can also load themselves recursively, allowing a form of fractal generation.

Examples

Check out provided examples to get a hang on API.

Adjust plugin configuration inside importers/map-scene.gd file.

Disable editor/import/use_multiple_threads for older versions of Godot.

Restart Godot if the plugin types fail to parse during the first launch.

Disable lightmap_unwrap setting if the freezes are consistent.

Version

1.0

Engine

4.2

Category

3D Tools

Download

Version1.0
Download Now

Support

If you need help or have questions about this plugin, please contact the author.

Contact Author