docs: community mapmaking documentation (7-file SDK guide)
- docs/mapmaking/00-index.md — SDK overview with workflow diagram - docs/mapmaking/01-getting-started.md — template setup & first map - docs/mapmaking/02-building-geometry.md — CSG guide & prefab reference - docs/mapmaking/03-lighting-and-env.md — LightmapGI baking guide - docs/mapmaking/04-validation.md — validator CLI & CI/CD usage - docs/mapmaking/05-packaging-and-shipping.md — .pck pipeline - docs/mapmaking/06-faq-and-troubleshooting.md — 30+ common issues - README.md — add mapmaking SDK link to features list
This commit is contained in:
@@ -5,6 +5,7 @@ A competitive, round-based tactical FPS built in Godot 4.
|
||||
- 128-tick authoritative dedicated servers
|
||||
- GDExtension (C++) simulation core
|
||||
- Community-hostable, self-hosted server browser
|
||||
- Map-making SDK for community maps
|
||||
- [Map-making SDK](docs/mapmaking/00-index.md) for community maps
|
||||
|
||||
See [PLANS/PROJECT_PLAN.md](PLANS/PROJECT_PLAN.md) for the full build plan.
|
||||
See [Mapmaking SDK docs](docs/mapmaking/00-index.md) to build and ship custom maps.
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
# Tactical Shooter — Mapmaking SDK
|
||||
|
||||
Everything you need to build, validate, package, and ship competitive maps for
|
||||
Tactical Shooter. This SDK is designed for the community — no C++, no GDextension
|
||||
tinkering required. You only need Godot 4 (any 4.x) and a text editor.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Copy the template project
|
||||
cp -r client/map_template/ maps/my_first_map/
|
||||
|
||||
# Rename the demo scene
|
||||
mv maps/my_first_map/template_map.tscn maps/my_first_map/de_my_map.tscn
|
||||
|
||||
# Open in Godot
|
||||
godot maps/my_first_map/project.godot
|
||||
```
|
||||
|
||||
Then read the [Getting Started guide](01-getting-started.md).
|
||||
|
||||
## What You Can Build
|
||||
|
||||
- **Competitive defuse maps** — A-site, B-site, CT/T spawns, buy zones
|
||||
- **Custom game modes** — The template is gameplay-agnostic; your map logic
|
||||
is in Godot groups, not hardcoded
|
||||
- **Community servers** — Ship maps as `.pck` files via the registry server
|
||||
|
||||
## SDK Components
|
||||
|
||||
| Component | Location | Purpose |
|
||||
|-----------|----------|---------|
|
||||
| [Template project](01-getting-started.md) | `client/map_template/` | Standalone Godot project with CSG prefabs and demo scene |
|
||||
| [CSG Prefabs](02-building-geometry.md) | `client/map_template/assets/prefabs/` | 6 drop-in node types (spawns, sites, zones) |
|
||||
| [Editor Validator](04-validation.md) | `client/map_template/template_map.gd` | Auto-runs when scene opened in editor |
|
||||
| [Headless Validator](04-validation.md) | `client/tools/validate_map.gd` | 4-module CLI validator for CI/CD |
|
||||
| [Packaging](05-packaging-and-shipping.md) | `scripts/map_packaging/pack_map.gd` | Exports `.tscn` → `.pck` addon pack |
|
||||
| [Registry Server](05-packaging-and-shipping.md) | `scripts/map_packaging/map_registry_server.py` | Serves `.pck` files over HTTP |
|
||||
| [Client Downloader](05-packaging-and-shipping.md) | `client/scripts/map_downloader.gd` | Autoload that downloads + loads `.pck` at runtime |
|
||||
|
||||
## Map Authoring Workflow
|
||||
|
||||
```
|
||||
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
||||
│ 1. Build │ ──→ │ 2. Validate │ ──→ │ 3. Package │
|
||||
│ - CSG geometry │ │ - Editor check │ │ - pack_map.gd │
|
||||
│ - Prefab nodes │ │ - CLI validator │ │ - → .pck file │
|
||||
│ - Lighting │ │ - Fix warnings │ │ │
|
||||
└──────────────────┘ └──────────────────┘ └──────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
||||
│ 6. Load & Play │ ←── │ 5. Download │ ←── │ 4. Upload │
|
||||
│ - Auto-download │ │ - Registry HTTP │ │ - map_registry │
|
||||
│ - changelevel │ │ - Cache to disk │ │ - .pck + .json │
|
||||
└──────────────────┘ └──────────────────┘ └──────────────────┘
|
||||
```
|
||||
|
||||
## Performance Targets
|
||||
|
||||
Maps built with the SDK must meet these budgets:
|
||||
|
||||
| Metric | Target | Enforced by |
|
||||
|------------------|-------------|--------------------------------|
|
||||
| Triangle count | ≤ 50,000 | `validate_polycount.gd` |
|
||||
| Texture size | ≤ 1024×1024 | `validate_textures.gd` |
|
||||
| Dynamic lights | ≤ 4 | `validate_lights.gd` |
|
||||
| LightmapGI | ✓ Baked | `validate_lights.gd` |
|
||||
| ReflectionProbe | ≥ 1 | `validate_lights.gd` (warning) |
|
||||
| FPS target | 60+ | Project rendering settings |
|
||||
|
||||
## File Structure Reference
|
||||
|
||||
```
|
||||
tactical-shooter/
|
||||
├── client/
|
||||
│ ├── project.godot # Main game project
|
||||
│ ├── map_template/ # ← YOU ARE HERE (template project)
|
||||
│ │ ├── project.godot # Standalone Godot project
|
||||
│ │ ├── template_map.tscn # Demo scene (rename me)
|
||||
│ │ ├── template_map.gd # Editor validation script
|
||||
│ │ └── assets/prefabs/ # CSG prefab nodes
|
||||
│ ├── tools/
|
||||
│ │ ├── validate_map.gd # CLI validator entry point
|
||||
│ │ └── validate_map/ # 4 validator modules
|
||||
│ │ ├── validate_scene.gd
|
||||
│ │ ├── validate_polycount.gd
|
||||
│ │ ├── validate_textures.gd
|
||||
│ │ └── validate_lights.gd
|
||||
│ └── scripts/
|
||||
│ └── map_downloader.gd # Client autoload for .pck loading
|
||||
├── scripts/
|
||||
│ └── map_packaging/
|
||||
│ ├── pack_map.gd # Godot editor → .pck exporter
|
||||
│ └── map_registry_server.py # Python HTTP registry server
|
||||
└── docs/mapmaking/
|
||||
├── 00-index.md # This document
|
||||
├── 01-getting-started.md # Template project setup
|
||||
├── 02-building-geometry.md # CSG & prefab guide
|
||||
├── 03-lighting-and-env.md # Lighting & baking
|
||||
├── 04-validation.md # Validator usage
|
||||
├── 05-packaging-and-shipping.md# Publishing pipeline
|
||||
└── 06-faq-and-troubleshooting.md
|
||||
```
|
||||
@@ -0,0 +1,135 @@
|
||||
# Getting Started — Your First Map
|
||||
|
||||
This guide walks you through setting up the map template project and creating
|
||||
a basic playable map.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Godot 4.0 or later (any 4.x stable)
|
||||
- The Tactical Shooter repository cloned
|
||||
- Familiarity with Godot's editor (scene tree, inspector, 3D viewport)
|
||||
|
||||
## 1. Copy the Template
|
||||
|
||||
The template project lives at `client/map_template/`. Copy it somewhere outside
|
||||
the main project (or keep it adjacent — it's a standalone Godot project):
|
||||
|
||||
```bash
|
||||
# From the tactical-shooter repository root
|
||||
cp -r client/map_template/ maps/my_first_map/
|
||||
cd maps/my_first_map/
|
||||
mv template_map.tscn de_dust2.tscn
|
||||
```
|
||||
|
||||
## 2. Open in Godot
|
||||
|
||||
Launch Godot and click **Import** (not Open). Navigate to your copied directory
|
||||
and select `project.godot`. The map template project will open.
|
||||
|
||||
> **Why "Import"?** Because template is a standalone project with its own
|
||||
> `project.godot`. You're importing a new project into the Godot project list,
|
||||
> not opening a scene within an existing project.
|
||||
|
||||
## 3. Explore the Demo Scene
|
||||
|
||||
Open `de_dust2.tscn` (the renamed `template_map.tscn`). You'll see:
|
||||
|
||||
- **Floor**: A green CSGBox3D pad (16×20 units)
|
||||
- **Walls**: Orange CSGBox3D walls forming a rough box
|
||||
- **Spawn pads**: Green (CT) and red (T) Marker3D spawn points
|
||||
- **Bomb sites**: Semi-transparent orange floor panels
|
||||
- **Buy zones**: Semi-transparent yellow trigger areas
|
||||
- **Lighting**: DirectionalLight3D (sun), OmniLight3D fill, LightmapGI
|
||||
- **Environment**: WorldEnvironment with sky
|
||||
|
||||
The scene also contains a `template_map.gd` editor tool script. When you open
|
||||
this scene, the **Output** panel (bottom dock) shows auto-validation results
|
||||
for the map structure.
|
||||
|
||||
## 4. Build Your First Room
|
||||
|
||||
1. **Select the floor** — Click `Floor` in the scene tree
|
||||
2. **Scale it** — With the scale tool (R), drag the red/green/blue handles
|
||||
to resize the floor to roughly 12×16 units
|
||||
3. **Add walls** — Select `Walls` → right-click → **Instantiate Child Scene** →
|
||||
select `assets/prefabs/map_bounds.tscn`. Size them to enclose the floor
|
||||
4. **Add a ceiling** — Duplicate (Ctrl+D) the floor, move it up 4 units,
|
||||
set **Operation** to Union (0) so it acts as a solid ceiling
|
||||
|
||||
> **Tip:** Use **View → Perspective** and rotate around to check your geometry
|
||||
> from all angles. Walls should overlap slightly to prevent light leaks.
|
||||
|
||||
## 5. Place Spawn Points
|
||||
|
||||
Spawn points tell the game where players appear at round start.
|
||||
|
||||
**CT Spawns:**
|
||||
- Drag `assets/prefabs/ct_spawn.tscn` from the FileSystem dock into the scene
|
||||
- Position it on the floor at one end of the map
|
||||
- The green pad's +Z arrow shows the direction players face on spawn
|
||||
- Duplicate for 5 players (competitive standard)
|
||||
|
||||
**T Spawns:**
|
||||
- Same process with `assets/prefabs/t_spawn.tscn` (red pads)
|
||||
- Place at the opposite end of the map
|
||||
|
||||
## 6. Define Bomb Sites
|
||||
|
||||
A competitive defuse map needs two bomb sites, A and B.
|
||||
|
||||
1. Drag `assets/prefabs/bomb_site.tscn` into the scene
|
||||
2. Rename it to `BombsiteA`
|
||||
3. Resize the CollisionShape3D to cover the intended site area
|
||||
4. In the **Node** dock → **Groups** tab, ensure it has:
|
||||
- `bomb_site` (required — game logic finds sites by this group)
|
||||
- `bombsite_a` (informational — helps identify which site)
|
||||
5. Repeat for BombsiteB with group `bombsite_b`
|
||||
|
||||
## 7. Add Buy Zones
|
||||
|
||||
Buy zones mark areas where players can purchase equipment.
|
||||
|
||||
1. Drag `assets/prefabs/buy_zone.tscn` into the scene
|
||||
2. Place one at the CT spawn area (group `buy_zone` stays)
|
||||
3. Resize the yellow CollisionShape3D to cover the spawn room
|
||||
4. Duplicate for T spawn — add subgroup `ct_buy` or `t_buy` on each
|
||||
|
||||
## 8. Check the Editor Validator
|
||||
|
||||
Open the **Output** (bottom dock). The `template_map.gd` script runs
|
||||
automatically. You should see something like:
|
||||
|
||||
```
|
||||
=== Map Template: Validate Scene ===
|
||||
+ CT Spawn points (5 found)
|
||||
+ T Spawn points (5 found)
|
||||
+ Buy zones (2 found)
|
||||
+ Bomb sites (2 found)
|
||||
+ Cubemap capture origins (1 found)
|
||||
+ Map boundary walls (8 found)
|
||||
+ CSG Floor geometry (1 nodes)
|
||||
+ CSG Wall geometry (4 walls)
|
||||
+ LightmapGI configured ✓
|
||||
+ LightmapGI: NOT YET BAKED
|
||||
+ ReflectionProbe present
|
||||
+ WorldEnvironment present
|
||||
+ DirectionalLight3D (sun) present
|
||||
```
|
||||
|
||||
The "NOT YET BAKED" warning is expected — you'll bake lighting after geometry
|
||||
is finalised (see [Lighting guide](03-lighting-and-env.md)).
|
||||
|
||||
## 9. Save and Package
|
||||
|
||||
When your map is ready for testing:
|
||||
|
||||
1. Save the scene (`Ctrl+S`)
|
||||
2. Read the [Validation guide](04-validation.md) for headless CI checks
|
||||
3. Read the [Packaging guide](05-packaging-and-shipping.md) to ship it
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Building Geometry with CSG](02-building-geometry.md) — detailed prefab reference
|
||||
- [Lighting & Baking](03-lighting-and-env.md) — how to light your map properly
|
||||
- [Validator Reference](04-validation.md) — CLI validator exit codes and CI
|
||||
- [Packaging & Shipping](05-packaging-and-shipping.md) — publishing as .pck
|
||||
@@ -0,0 +1,245 @@
|
||||
# Building Geometry with CSG
|
||||
|
||||
Tactical Shooter maps are built with Godot's **CSG** (Constructive Solid
|
||||
Geometry) system — the same tool you'd use in a 3D modeller, but inside the
|
||||
Godot editor. CSG nodes let you cut, join, and extrude geometry in real-time
|
||||
with instant visual feedback.
|
||||
|
||||
## CSG Basics
|
||||
|
||||
### CSG Node Types
|
||||
|
||||
| Node | Use | Shape |
|
||||
|------|-----|-------|
|
||||
| `CSGBox3D` | Walls, floors, ceilings, pillars | Rectangular prism |
|
||||
| `CSGSphere3D` | Domes, curved edges | Sphere |
|
||||
| `CSGCylinder3D` | Columns, pipes | Cylinder |
|
||||
| `CSGCombiner3D` | Group container, non-collidable parent | — |
|
||||
| `CSGPolygon3D` | Extruded 2D path | Custom shape |
|
||||
|
||||
### CSG Operations
|
||||
|
||||
Each CSG node has an **Operation** property that determines how it combines
|
||||
with sibling CSG nodes in the same parent:
|
||||
|
||||
| Value | Operation | Effect |
|
||||
|-------|-----------|--------|
|
||||
| 0 | **Union** | Adds to existing geometry (default) |
|
||||
| 1 | **Subtraction** | Cuts a hole through parent |
|
||||
| 2 | **Intersection** | Keeps only overlapping volume |
|
||||
|
||||
**Example — a window cutout:**
|
||||
|
||||
```
|
||||
Wall (CSGBox3D, Union)
|
||||
└── WindowCutout (CSGBox3D, Subtraction)
|
||||
```
|
||||
|
||||
This subtracts the window shape from the wall. Move/resize the subtraction
|
||||
child to reposition the window.
|
||||
|
||||
### Key Properties
|
||||
|
||||
- **`use_collision`** (bool) — Must be `true` on all floor/wall/ceiling CSG
|
||||
nodes so players and bullets collide with them
|
||||
- **`use_shadows`** (bool) — Enable shadow-casting on geometry
|
||||
- **`material`** — Assign placeholder materials during construction; final
|
||||
materials come from the main game project library
|
||||
- **`snap_mode`** — Controls vertex snapping (useful for aligning geometry)
|
||||
|
||||
## Building a Room
|
||||
|
||||
A basic room needs 3 CSG layers:
|
||||
|
||||
```
|
||||
Room (CSGCombiner3D)
|
||||
├── Floor (CSGBox3D, Union, use_collision=true)
|
||||
├── Walls (CSGCombiner3D)
|
||||
│ ├── Wall_North (CSGBox3D, Union)
|
||||
│ ├── Wall_South (CSGBox3D, Union)
|
||||
│ ├── Wall_East (CSGBox3D, Union)
|
||||
│ └── Wall_West (CSGBox3D, Union)
|
||||
└── Ceiling (CSGBox3D, Union, use_collision=true)
|
||||
```
|
||||
|
||||
### CSG Building Tips
|
||||
|
||||
1. **Overlap slightly** — Walls should extend past the floor edge by ~0.1 units
|
||||
to prevent light leaks (thin gaps let light bleed through)
|
||||
2. **Keep CSG simple** — Use as few CSG nodes as possible. A wall panel is one
|
||||
CSGBox3D, not 4 extruded edges
|
||||
3. **Convert to Mesh when done** — Once geometry is final, select all CSG
|
||||
nodes → right-click → **Convert CSG to Mesh**. This bakes them into static
|
||||
MeshInstance3D nodes that perform much better
|
||||
4. **Avoid curved CSG for floor/walls** — CSGSphere3D and CSGCylinder3D are
|
||||
expensive. Use them sparingly
|
||||
|
||||
## Prefab Reference
|
||||
|
||||
The template provides 6 drag-and-drop prefab nodes. Each is an `assets/prefabs/*.tscn`
|
||||
file that you instantiate into your scene.
|
||||
|
||||
### ct_spawn.tscn
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Type | `Marker3D` (green pad + arrow) |
|
||||
| Group | `ct_spawn` |
|
||||
| Purpose | CT team spawn position |
|
||||
| Quantity | 5 (one per player) |
|
||||
|
||||
The +Z arrow (blue axis) shows the direction players face when they spawn.
|
||||
Position the pad at floor level — make it flush with the floor surface.
|
||||
|
||||
**Customisation:** Duplicate instances rather than editing the prefab. Adjust
|
||||
only position and rotation. The green pad is cosmetic.
|
||||
|
||||
### t_spawn.tscn
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Type | `Marker3D` (red pad + arrow) |
|
||||
| Group | `t_spawn` |
|
||||
| Purpose | Terrorist team spawn position |
|
||||
| Quantity | 5 |
|
||||
|
||||
Same usage as `ct_spawn` but red. Place at the opposite end of the map.
|
||||
|
||||
### buy_zone.tscn
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Type | `Area3D` with `CollisionShape3D` |
|
||||
| Group | `buy_zone` |
|
||||
| Purpose | Purchase-eligible zone |
|
||||
| Quantity | 1–2 |
|
||||
|
||||
The game monitors `body_entered` / `body_exited` on these areas to show/hide
|
||||
the buy menu. Resize the child CollisionShape3D to match the room shape.
|
||||
|
||||
**Additional groups (optional):**
|
||||
- `ct_buy` — Only CT team can buy in this zone
|
||||
- `t_buy` — Only T team can buy in this zone
|
||||
|
||||
If omitted, any player inside a `buy_zone` can buy (suitable for shared mid areas).
|
||||
|
||||
### bomb_site.tscn
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Type | `Area3D` with `CollisionShape3D` |
|
||||
| Group | `bomb_site` |
|
||||
| Purpose | Bomb plant zone |
|
||||
| Quantity | 2 (A and B) |
|
||||
|
||||
Rename instances to `BombsiteA` and `BombsiteB`. Resize the CollisionShape3D
|
||||
to cover the plantable area. Visual: semi-transparent orange floor panel.
|
||||
|
||||
**Additional groups (strongly recommended):**
|
||||
- `bombsite_a` — Identifies this as site A
|
||||
- `bombsite_b` — Identifies this as site B
|
||||
|
||||
### cubemap_origin.tscn
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Type | `Node3D` (blue sphere marker) |
|
||||
| Group | `cubemap_origin` |
|
||||
| Purpose | Marks ReflectionProbe capture position |
|
||||
| Quantity | 1 |
|
||||
|
||||
Place at eye height ≈ 1.6 units above the floor in the most visually prominent
|
||||
area (usually mid-map or the central choke point). The ReflectionProbe is a
|
||||
separate node that should be placed at this marker's position after geometry
|
||||
is finalised.
|
||||
|
||||
### map_bounds.tscn
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Type | `Area3D` with `CollisionShape3D` |
|
||||
| Group | `map_bounds` |
|
||||
| Purpose | Out-of-bounds kill wall |
|
||||
| Quantity | 4–8 (enclose the perimeter) |
|
||||
|
||||
**Usage:**
|
||||
- Place one instance per perimeter face (north, south, east, west)
|
||||
- Resize each CollisionShape3D to form a closed box around the playable area
|
||||
- Make the floor-bound wall tall enough that players can't jump over it
|
||||
- The game detects `body_exited` from these areas and teleports/kills
|
||||
out-of-bounds players
|
||||
|
||||
> **Performance tip:** `map_bounds` nodes should be as thin as practical —
|
||||
> these are just detection triggers, not visual geometry. A thickness of 0.5
|
||||
> units is sufficient.
|
||||
|
||||
## Advanced Geometry Techniques
|
||||
|
||||
### Corridors and Hallways
|
||||
|
||||
Build a corridor as a CSGCombiner3D with union floor + walls + ceiling and
|
||||
subtraction nodes for doors/windows:
|
||||
|
||||
```
|
||||
Corridor (CSGCombiner3D)
|
||||
├── Floor (CSGBox3D, Union)
|
||||
├── LeftWall (CSGBox3D, Union)
|
||||
│ └── Door1 (CSGBox3D, Subtraction)
|
||||
├── RightWall (CSGBox3D, Union)
|
||||
│ └── Window1 (CSGBox3D, Subtraction)
|
||||
└── Ceiling (CSGBox3D, Union)
|
||||
```
|
||||
|
||||
### Ramps and Stairs
|
||||
|
||||
Use multiple CSGBox3D nodes at increasing heights. Stack them like steps and
|
||||
wrap in a CSGCombiner3D for a clean hierarchy:
|
||||
|
||||
```
|
||||
Stairs (CSGCombiner3D)
|
||||
├── Step1 (CSGBox3D, Union, pos=(0, 0, 0))
|
||||
├── Step2 (CSGBox3D, Union, pos=(0, 0.5, 1))
|
||||
├── Step3 (CSGBox3D, Union, pos=(0, 1.0, 2))
|
||||
└── Step4 (CSGBox3D, Union, pos=(0, 1.5, 3))
|
||||
```
|
||||
|
||||
For ramps, rotate a CSGBox3D or use CSGPolygon3D with an extruded triangle shape.
|
||||
|
||||
### Map Scale Reference
|
||||
|
||||
| Element | Size (units) | Notes |
|
||||
|---------|-------------|-------|
|
||||
| Player height | 1.8 | Eye level ≈ 1.6 |
|
||||
| Player width | 0.5 | Collision capsule radius |
|
||||
| Doorway | 1.5 × 2.5 | Wide enough for 2 players |
|
||||
| Corridor | 3–4 wide | Comfortable movement |
|
||||
| Bombsite | 8×8 minimum | Playable site area |
|
||||
| Spawn room | 6×8 | 5 players need space |
|
||||
| Mid area | 6–8 wide | Main chokepoint |
|
||||
|
||||
## From CSG to MeshInstance3D (Baking)
|
||||
|
||||
When your geometry is **final** (no more changes expected):
|
||||
|
||||
1. Select all CSG nodes in the scene tree
|
||||
2. Right-click → **Convert CSG to Mesh**
|
||||
3. This replaces CSG nodes with static `MeshInstance3D` nodes
|
||||
4. The mesh instances are much more performant
|
||||
|
||||
**Don't convert until you're done** — CSG is editable, MeshInstance3D is not.
|
||||
Keep working in CSG mode; convert only for final optimisation before packaging.
|
||||
|
||||
## Performance Budget
|
||||
|
||||
See the [Validator guide](04-validation.md) for detailed numbers, but the key
|
||||
geometry rules are:
|
||||
|
||||
- **≤ 50,000 triangles** total across all meshes
|
||||
- **≤ 5,000 triangles** per individual mesh (recommended; enforced as warning)
|
||||
- Use CSG for blockout only — convert to mesh before shipping
|
||||
- Avoid CSGSphere3D in visible areas; use hand-modelled meshes
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Lighting & Environment Setup](03-lighting-and-env.md) — light your map
|
||||
- [Map Validation](04-validation.md) — run the validator on your scene
|
||||
@@ -0,0 +1,159 @@
|
||||
# Lighting & Environment Setup
|
||||
|
||||
Proper lighting is the difference between a map that looks professional and one
|
||||
that looks like a grey box. Tactical Shooter uses Godot's **LightmapGI** system
|
||||
for baked global illumination: light bounces are pre-calculated and stored in
|
||||
lightmaps, giving you realistic lighting at zero runtime cost.
|
||||
|
||||
## Lighting Architecture
|
||||
|
||||
```
|
||||
DirectionalLight3D (sun) — main directional light, shadows
|
||||
└─ OmniLight3D (fill) — ambient fill lights, no shadow
|
||||
└─ LightmapGI — bakes all static lighting into textures
|
||||
└─ baked lightmaps (zero runtime cost)
|
||||
```
|
||||
|
||||
## 1. WorldEnvironment
|
||||
|
||||
Every map needs a `WorldEnvironment` node. This controls:
|
||||
|
||||
- **Sky** — Background appearance (skybox or solid colour)
|
||||
- **Ambient light** — Base light level for unlit surfaces
|
||||
- **Tonemapping** — Colour grading and exposure
|
||||
|
||||
**Setup:**
|
||||
1. The template already includes a `WorldEnvironment` node
|
||||
2. If creating from scratch, add one via the scene tree → + button → `WorldEnvironment`
|
||||
3. In the Inspector, assign an `Environment` resource (New Environment)
|
||||
4. Configure:
|
||||
- **Background:** Sky (for outdoor) or Clear Color (for underground maps)
|
||||
- **Ambient Light:** Sky (mimics sky colour) or Color (custom ambient)
|
||||
- **Tonemap:** Reinhard or Filmic (prevents blown-out highlights)
|
||||
|
||||
> The validator warns if WorldEnvironment is missing — add it early.
|
||||
|
||||
## 2. DirectionalLight3D (Sun)
|
||||
|
||||
The main sun light. One per map.
|
||||
|
||||
**Recommended settings:**
|
||||
| Property | Value | Reason |
|
||||
|----------|-------|--------|
|
||||
| `light_bake_mode` | Static (2) | Bakes into lightmap |
|
||||
| `shadow_enabled` | true | Required for competitive visibility |
|
||||
| `shadow_bias` | 0.1 | Prevents shadow acne on CSG |
|
||||
| `angular_distance` | 0.01 | Sharp sunlight (competitive visual clarity) |
|
||||
| `range` | 50–100 | Large enough to cover the map |
|
||||
|
||||
**Positioning:**
|
||||
- Rotate the sun to create interesting shadows through windows/doorways
|
||||
- Avoid straight-down lighting (makes everything look flat)
|
||||
- A 45° angle works well for most maps
|
||||
|
||||
## 3. Fill Lights (OmniLight3D)
|
||||
|
||||
Fill lights illuminate areas the sun doesn't reach (interiors, tunnels).
|
||||
|
||||
**Rules:**
|
||||
- **Maximum 4 dynamic (non-baked) lights** — enforced by the validator
|
||||
- Fill lights should use `light_bake_mode = Static (2)` so they bake into
|
||||
the lightmap and don't count toward the dynamic budget
|
||||
- Use low intensity (0.3–0.5) for ambient fill
|
||||
|
||||
**Performance tip:** Every omni/spot light with `shadow_enabled = true` is
|
||||
expensive. The validator warns on dynamic lights with shadows. Baked lights
|
||||
with shadows disabled are free — use them.
|
||||
|
||||
## 4. LightmapGI — Baked Global Illumination
|
||||
|
||||
LightmapGI is the backbone of Tactical Shooter's lighting. It pre-computes
|
||||
light bouncing and stores the result as textures (lightmaps).
|
||||
|
||||
### Configuration
|
||||
|
||||
| Property | Recommended | Notes |
|
||||
|----------|-------------|-------|
|
||||
| `quality` | 2 (High) | Higher = better GI, longer bake |
|
||||
| `bounces` | 3 | Minimum 2 for realistic indirect lighting |
|
||||
| `texel_scale` | 1.0–2.0 | Lower = higher res, longer bake |
|
||||
| `max_texture_size` | 1024 | Controls lightmap atlas size |
|
||||
| `use_denoiser` | true | Removes noise from baked result |
|
||||
| `environment_mode` | 0 (Baked Environment) | Uses WorldEnvironment for sky lighting |
|
||||
|
||||
### Baking
|
||||
|
||||
To bake: select the `LightmapGI` node → click **Bake Lightmap** in the top
|
||||
toolbar (or `Ctrl+Shift+C`).
|
||||
|
||||
**Baking checklist:**
|
||||
- [ ] All geometry is finalised (moving geometry after baking invalidates it)
|
||||
- [ ] All lights have correct bake mode (Static for baked, Dynamic for real-time)
|
||||
- [ ] CSG has `use_collision = true` (non-collidable CSG may not bake properly)
|
||||
- [ ] WorldEnvironment is configured with a sky or ambient colour
|
||||
- [ ] Save the scene before baking
|
||||
|
||||
**Baking time:** Varies by map complexity. A small map (10×14 units) bakes in
|
||||
30–60 seconds. A full competitive map (20×20) may take 5–10 minutes.
|
||||
|
||||
### After Baking
|
||||
|
||||
- The LightmapGI node's `light_data` property will show a `LightmapGIData` resource
|
||||
- The validator checks for `light_data != null` to confirm the map is baked
|
||||
- Baked lightmaps are stored in the `.tscn` file as embedded resources
|
||||
- You can rebake at any time — it overwrites the previous lightmaps
|
||||
|
||||
**Warning:** Opening an unbaked map on a server will default to unlit rendering.
|
||||
Always bake before packaging.
|
||||
|
||||
## 5. ReflectionProbe
|
||||
|
||||
ReflectionProbes add specular reflections to shiny surfaces (metal, glass, water).
|
||||
|
||||
- Place one `ReflectionProbe` near the `cubemap_origin` marker
|
||||
- Set `max_distance` to cover the visible area (10–30 units)
|
||||
- Enable `box_projection` for interior spaces (corrects reflection distortion)
|
||||
- Set `intensity` to 0.5–0.8 (realistic reflection strength)
|
||||
|
||||
**Tip:** You need reflection probes even if your map has no mirrors — they make
|
||||
metal and plastic materials look correct.
|
||||
|
||||
## 6. Lighting Performance Budget
|
||||
|
||||
| Metric | Limit | Enforced |
|
||||
|--------|-------|----------|
|
||||
| Dynamic (non-baked) lights | ≤ 4 | Validator (error) |
|
||||
| Total lights (baked + dynamic) | Unlimited | — |
|
||||
| LightmapGI bounces | ≥ 2 | Validator (warning) |
|
||||
| ReflectionProbes | ≥ 1 | Validator (warning) |
|
||||
| Lightmap max texture | ≤ 2048 | Project settings |
|
||||
|
||||
If you exceed the dynamic light budget, set more lights to
|
||||
`light_bake_mode = Static (2)` — baked lights cost nothing at runtime.
|
||||
|
||||
## 7. Quick Start Checklist
|
||||
|
||||
- [ ] WorldEnvironment with Environment resource assigned
|
||||
- [ ] DirectionalLight3D (sun) with shadows
|
||||
- [ ] 1–3 fill OmniLight3D with Static bake mode
|
||||
- [ ] LightmapGI with quality=2, bounces=3, texel_scale=1.0
|
||||
- [ ] ReflectionProbe at cubemap origin
|
||||
- [ ] All geometry CSG has `use_collision = true`
|
||||
- [ ] Save scene → Bake LightmapGI (Ctrl+Shift+C)
|
||||
- [ ] Verify `light_data` is populated (not null)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Likely Cause | Fix |
|
||||
|---------|-------------|-----|
|
||||
| Light leaks at corners | CSG gap < 0.01 units | Overlap CSG edges by 0.1 |
|
||||
| Baked lighting too dark | bounces too low | Set bounces ≥ 2 |
|
||||
| Lightmap seams visible | texel_scale too high | Lower texel_scale to 1.0 |
|
||||
| Shadow acne (striped shadows) | shadow_bias too low | Increase shadow_bias to 0.1 |
|
||||
| ReflectionProbe visible as sphere | probe_mode wrong | Set to Box or disable visibility |
|
||||
| Bake fails with error | Scene not saved | Save `.tscn` before baking |
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Map Validation](04-validation.md) — run all checks on your scene
|
||||
- [Packaging & Shipping](05-packaging-and-shipping.md) — publish your map
|
||||
@@ -0,0 +1,235 @@
|
||||
# Map Validation
|
||||
|
||||
The validator catches structural problems, performance budget violations, and
|
||||
lighting issues before your map ships. Two validation modes are available:
|
||||
|
||||
1. **Editor validation** — runs automatically when you open a map scene
|
||||
2. **Headless CLI validation** — runs from the terminal, suitable for CI/CD
|
||||
|
||||
## Editor Validation (template_map.gd)
|
||||
|
||||
Every map created from the template includes `template_map.gd`, a `@tool`
|
||||
script that runs automatically when the scene opens in the Godot editor.
|
||||
|
||||
**How to use it:**
|
||||
1. Open your map's `.tscn` file in the Godot editor
|
||||
2. Look at the **Output** panel (bottom dock)
|
||||
3. The script prints a validation report like:
|
||||
```
|
||||
=== Map Template: Validate Scene ===
|
||||
+ CT Spawn points (5 found)
|
||||
+ T Spawn points (5 found)
|
||||
+ Buy zones (2 found)
|
||||
+ Bomb sites (2 found)
|
||||
+ Cubemap capture origins (1 found)
|
||||
+ Map boundary walls (8 found)
|
||||
+ LightmapGI configured ✓
|
||||
+ LightmapGI: NOT YET BAKED
|
||||
+ DirectionalLight3D (sun) present
|
||||
```
|
||||
|
||||
**Checks performed:**
|
||||
- Required gameplay groups exist (`ct_spawn`, `t_spawn`, `buy_zone`, etc.)
|
||||
- CSG floor and wall geometry present
|
||||
- LightmapGI configured and baked
|
||||
- ReflectionProbe and WorldEnvironment present
|
||||
- DirectionalLight3D (sun) present
|
||||
- Playable area extents estimated
|
||||
|
||||
## Headless CLI Validator (validate_map.gd)
|
||||
|
||||
The headless validator is a 4-module CLI tool at `client/tools/validate_map.gd`.
|
||||
Use it for automated checking before packaging or in CI/CD pipelines.
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
# From the tactical-shooter repository root
|
||||
godot --path client --script tools/validate_map.gd -- res://maps/mymap.tscn
|
||||
```
|
||||
|
||||
> The `--` separates Godot engine arguments from user arguments.
|
||||
> The scene path must be a `res://`-prefixed Godot resource path.
|
||||
|
||||
### Exit Codes
|
||||
|
||||
| Code | Meaning | Description |
|
||||
|------|---------|-------------|
|
||||
| 0 | **PASS** | All checks passed, no warnings |
|
||||
| 1 | **WARNINGS** | Passed with non-blocking suggestions |
|
||||
| 2 | **ERRORS** | Failed — must-fix items present |
|
||||
|
||||
### CI/CD Integration
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Gate map merges on validation
|
||||
|
||||
godot --path client --script tools/validate_map.gd -- res://maps/mymap.tscn
|
||||
exit_code=$?
|
||||
|
||||
if [ $exit_code -eq 2 ]; then
|
||||
echo "❌ Map validation FAILED — fix errors before submitting"
|
||||
exit 1
|
||||
elif [ $exit_code -eq 1 ]; then
|
||||
echo "⚠️ Map passed with warnings — review suggestions"
|
||||
fi
|
||||
|
||||
echo "✅ Map validated successfully"
|
||||
exit 0
|
||||
```
|
||||
|
||||
### Example Output
|
||||
|
||||
```
|
||||
═══════════════════════════════════════════
|
||||
MAP VALIDATOR
|
||||
Scene: res://maps/de_dust2.tscn
|
||||
═══════════════════════════════════════════
|
||||
|
||||
── [validate_scene] ────────────────────────────
|
||||
✓ Root node: World (Node3D)
|
||||
✓ WorldEnvironment (WorldEnvironment)
|
||||
✓ LightmapGI (LightmapGI)
|
||||
✓ Group "ct_spawn" → CT_SpawnPos (5 node(s))
|
||||
✓ Group "t_spawn" → T_SpawnPos (5 node(s))
|
||||
✓ Group "buy_zone" → BuyZone_A (2 node(s))
|
||||
✓ Group "bomb_site" → BombsiteA (2 node(s))
|
||||
✓ Group "cubemap_origin" → CubemapOrigin (1 node(s))
|
||||
✓ pass
|
||||
|
||||
── [validate_polycount] ────────────────────────
|
||||
Meshes scanned: 24
|
||||
Total triangles: 12,842
|
||||
Budget: 50,000
|
||||
✓ Within budget (25.7% of 50000)
|
||||
Top meshes by triangle count:
|
||||
4200 Building_01 (MeshInstance3D)
|
||||
2100 Wall_Detail (MeshInstance3D)
|
||||
1042 Floor_Tiles (MeshInstance3D)
|
||||
✓ pass
|
||||
|
||||
── [validate_textures] ─────────────────────────
|
||||
Textures inspected: 18
|
||||
✓ All textures within 1024×1024 limit
|
||||
✓ pass
|
||||
|
||||
── [validate_lights] ───────────────────────────
|
||||
Light breakdown:
|
||||
DirectionalLight3D: 1
|
||||
OmniLight3D: 2
|
||||
SpotLight3D: 0
|
||||
Dynamic (non-baked): 1
|
||||
Baked: 2
|
||||
✓ Dynamic light count: 1 (limit: 4)
|
||||
✓ LightmapGI baked (light_data present)
|
||||
Quality: 2 (0=Low, 1=Med, 2=High, 3=Ultra)
|
||||
Bounces: 3
|
||||
Texel Scale: 1.0
|
||||
Max Texture Size: 1024
|
||||
✓ WorldEnvironment configured
|
||||
ReflectionProbes: 1
|
||||
✓ pass
|
||||
|
||||
═══════════════════════════════════════════
|
||||
SUMMARY
|
||||
═══════════════════════════════════════════
|
||||
[PASS] validate_scene
|
||||
[PASS] validate_polycount
|
||||
[PASS] validate_textures
|
||||
[PASS] validate_lights
|
||||
|
||||
Errors: 0
|
||||
Warnings: 0
|
||||
Result: PASS
|
||||
```
|
||||
|
||||
## Validator Modules
|
||||
|
||||
### 1. Scene Structure (`validate_scene.gd`)
|
||||
|
||||
**What it checks:**
|
||||
- Scene root is a `Node3D` type
|
||||
- `WorldEnvironment` node present and named correctly
|
||||
- `LightmapGI` node present and named correctly
|
||||
- All 5 required groups assigned (`ct_spawn`, `t_spawn`, `buy_zone`, `bomb_site`, `cubemap_origin`)
|
||||
- Node names follow PascalCase convention (no spaces)
|
||||
- No duplicate required groups on different nodes
|
||||
|
||||
**Typical errors:**
|
||||
```
|
||||
✖ Missing required group: "ct_spawn"
|
||||
✖ Missing WorldEnvironment — map needs a WorldEnvironment node
|
||||
✖ Node name contains spaces: "My Room Section"
|
||||
```
|
||||
|
||||
### 2. Polygon Count (`validate_polycount.gd`)
|
||||
|
||||
**What it checks:**
|
||||
- Total triangle count across all `MeshInstance3D` nodes ≤ **50,000**
|
||||
- Per-mesh triangle breakdown printed (helps identify heavy assets)
|
||||
- Warns on individual meshes > **5,000** triangles
|
||||
- Warns if no `MeshInstance3D` nodes found at all
|
||||
|
||||
**Typical errors:**
|
||||
```
|
||||
✖ Map exceeds 50,000 triangle budget: 68,420 total (36.8% over budget)
|
||||
```
|
||||
|
||||
### 3. Texture Sizes (`validate_textures.gd`)
|
||||
|
||||
**What it checks:**
|
||||
- All material textures ≤ **1024×1024**
|
||||
- Checks `StandardMaterial3D`, `ORMMaterial3D`, and `ShaderMaterial` texture params
|
||||
- Warns on textures > **512×512** (suggested for secondary surfaces)
|
||||
- LightmapGI textures checked separately (baked textures can be larger)
|
||||
|
||||
**Typical errors:**
|
||||
```
|
||||
✖ Texture exceeds 1024×1024: floor_tiles.png (albedo) — 2048×
|
||||
```
|
||||
|
||||
### 4. Lights & Lightmap (`validate_lights.gd`)
|
||||
|
||||
**What it checks:**
|
||||
- Dynamic (non-baked) OmniLight3D + SpotLight3D ≤ **4**
|
||||
- DirectionalLight3D counts toward dynamic budget if not baked
|
||||
- LightmapGI present and baked (`light_data != null`)
|
||||
- Lightmap quality, bounces, texel scale, max texture size
|
||||
- WorldEnvironment assigned with Environment resource
|
||||
- ReflectionProbe presence and `max_distance` coverage
|
||||
- Warns on dynamic lights with shadow enabled (performance cost)
|
||||
|
||||
**Typical errors:**
|
||||
```
|
||||
✖ Too many dynamic lights: 6 (max 4)
|
||||
✖ No LightmapGI node found
|
||||
✖ No WorldEnvironment node found
|
||||
```
|
||||
|
||||
## Passing the Validator
|
||||
|
||||
The goal is exit code 0: **PASS**, no errors, no warnings.
|
||||
|
||||
| Code | Action |
|
||||
|------|--------|
|
||||
| 0 (PASS) | Your map is ready to package |
|
||||
| 1 (WARNINGS) | Fix suggestions before shipping (non-blocking) |
|
||||
| 2 (ERRORS) | Fix all errors — map must pass before packaging |
|
||||
|
||||
> **CI gate rule:** Treat exit code 2 as a hard block. Exit code 1 is advisory
|
||||
> but should be reviewed. Only exit code 0 means "ship it."
|
||||
|
||||
## Adding a Custom Validator Module
|
||||
|
||||
Advanced users can extend the validator:
|
||||
|
||||
1. Create `client/tools/validate_map/validate_<name>.gd` extending `RefCounted`
|
||||
2. Implement `func validate(scene_root: Node, scene_path: String) -> Dictionary`
|
||||
3. Return `{"pass": bool, "errors": [String], "warnings": [String]}`
|
||||
4. Add a `_run_module("<name>", instance, scene_path)` call in
|
||||
`validate_map.gd:_ready()`
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Packaging & Shipping](05-packaging-and-shipping.md) — ship your validated map
|
||||
@@ -0,0 +1,264 @@
|
||||
# Packaging & Shipping
|
||||
|
||||
Once your map is built, validated, and baked, it's time to publish it to the
|
||||
world. Tactical Shooter uses the **PCK** (Godot Resource Pack) format — the map
|
||||
is packaged as a `.pck` file, uploaded to a registry server, and clients
|
||||
download it automatically when they join a server running that map.
|
||||
|
||||
## Pipeline Overview
|
||||
|
||||
```
|
||||
┌──────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────────┐
|
||||
│ pack_map │ → │ .pck + .json │ → │ Map Registry │ → │ Client │
|
||||
│ .gd │ │ files │ → │ Server │ → │ Downloader │
|
||||
└──────────┘ └──────────────┘ └──────────────┘ └────────────┘
|
||||
(editor (artifacts) (HTTP serving) (autoload)
|
||||
tool)
|
||||
```
|
||||
|
||||
## Step 1: Package the Map
|
||||
|
||||
The `pack_map.gd` script (at `scripts/map_packaging/pack_map.gd`) is a Godot
|
||||
editor tool that exports a `.tscn` scene as a standalone `.pck` resource pack.
|
||||
|
||||
### Usage
|
||||
|
||||
From the **map template project** (not the main game project):
|
||||
|
||||
```bash
|
||||
cd maps/my_first_map/
|
||||
|
||||
godot --headless --script /path/to/scripts/map_packaging/pack_map.gd \
|
||||
--map=res://de_my_map.tscn
|
||||
```
|
||||
|
||||
> Replace `/path/to/` with the actual path to the tactical-shooter repository
|
||||
> root, or copy `pack_map.gd` into your map project's `scripts/` directory.
|
||||
|
||||
### What It Does
|
||||
|
||||
1. Loads the specified `.tscn` scene
|
||||
2. Recursively collects all dependencies (materials, textures, meshes, etc.)
|
||||
3. Exports them to `user://packed_maps/de_my_map.pck`
|
||||
4. Writes a metadata file `user://packed_maps/de_my_map.json`
|
||||
|
||||
### Output Files
|
||||
|
||||
```
|
||||
user://packed_maps/
|
||||
├── de_my_map.pck # Binary resource pack (~1–10 MB for a typical map)
|
||||
└── de_my_map.json # JSON metadata (machine-readable)
|
||||
```
|
||||
|
||||
**Metadata example** (`de_my_map.json`):
|
||||
```json
|
||||
{
|
||||
"map_name": "de_my_map",
|
||||
"source_scene": "res://de_my_map.tscn",
|
||||
"godot_version": {
|
||||
"major": 4,
|
||||
"minor": 2,
|
||||
"string": "4.2"
|
||||
},
|
||||
"packed_at": "2026-06-25T14:30:00"
|
||||
}
|
||||
```
|
||||
|
||||
### Requirements
|
||||
|
||||
- The map scene must use **local resources** only (relative `res://` paths)
|
||||
- External dependencies (game shared assets like weapon models, player
|
||||
models) should stay in the base game — the `.pck` is **additive** content
|
||||
- The `.pck` contains only the map's unique assets, not the entire game
|
||||
|
||||
### From the Editor
|
||||
|
||||
You can also run `pack_map.gd` from the Godot editor:
|
||||
|
||||
1. Open your map project in Godot
|
||||
2. Open your map scene (`de_my_map.tscn`)
|
||||
3. **Project → Tools → Pack Current Map** (if configured)
|
||||
4. The script detects the active scene and exports it
|
||||
|
||||
## Step 2: Set Up the Map Registry Server
|
||||
|
||||
The registry server is a lightweight Python HTTP server
|
||||
(`scripts/map_packaging/map_registry_server.py`) that:
|
||||
- Serves `.pck` files for download
|
||||
- Provides a JSON `/maps` endpoint listing available maps
|
||||
- Auto-scans the maps directory every 5 seconds
|
||||
- Computes and serves SHA-256 checksums for integrity verification
|
||||
|
||||
### Starting the Server
|
||||
|
||||
```bash
|
||||
cd /path/to/tactical-shooter
|
||||
|
||||
# Default: port 8090, maps from ./packed_maps/
|
||||
python3 scripts/map_packaging/map_registry_server.py
|
||||
|
||||
# Custom configuration
|
||||
python3 scripts/map_packaging/map_registry_server.py \
|
||||
--port 8080 \
|
||||
--maps-dir /data/tactical-shooter-maps \
|
||||
--verbose
|
||||
```
|
||||
|
||||
**Environment variables:**
|
||||
| Variable | Overrides | Default |
|
||||
|----------|-----------|---------|
|
||||
| `MAP_REGISTRY_PORT` | `--port` / `-p` | 8090 |
|
||||
| `MAP_REGISTRY_MAPS` | `--maps-dir` / `-d` | `./packed_maps/` |
|
||||
|
||||
### API Endpoints
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `GET /` | — | Server info and endpoint documentation |
|
||||
| `GET /maps` | — | JSON list of all available maps with metadata |
|
||||
| `GET /maps/<name>.pck` | — | Download map `.pck` binary |
|
||||
| `GET /maps/<name>.json` | — | Download per-map metadata |
|
||||
| `OPTIONS /maps` | — | CORS preflight |
|
||||
|
||||
**Example responses:**
|
||||
|
||||
```
|
||||
GET /maps
|
||||
```
|
||||
```json
|
||||
{
|
||||
"maps": [
|
||||
{
|
||||
"name": "de_dust2",
|
||||
"size": 5432100,
|
||||
"version": 1,
|
||||
"description": "Classic competitive map",
|
||||
"checksum_sha256": "a1b2c3d4...",
|
||||
"packed_at": "2026-06-25T14:30:00"
|
||||
}
|
||||
],
|
||||
"server_name": "Tactical Shooter Map Registry",
|
||||
"server_version": "1.0.0",
|
||||
"map_count": 1
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
GET /maps/de_dust2.pck
|
||||
```
|
||||
→ Returns the `.pck` binary file as an octet-stream download.
|
||||
|
||||
### Deployment Options
|
||||
|
||||
- **Local dev:** Run on the same machine as the game (default `127.0.0.1:8090`)
|
||||
- **Server host:** Run on a public server alongside the master server
|
||||
- **Docker:** Wrap in a Docker container for easy deployment
|
||||
- **Systemd service:** Create a service file for production hosting
|
||||
|
||||
**Systemd service example:**
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Tactical Shooter Map Registry
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=tactical
|
||||
WorkingDirectory=/opt/tactical-shooter
|
||||
ExecStart=/usr/bin/python3 scripts/map_packaging/map_registry_server.py \
|
||||
--port 8090 --maps-dir /data/tactical-maps
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
## Step 3: Upload Your Map
|
||||
|
||||
1. Copy the `.pck` and `.json` files to the registry server's maps directory:
|
||||
```bash
|
||||
cp user://packed_maps/de_my_map.pck /data/tactical-maps/
|
||||
cp user://packed_maps/de_my_map.json /data/tactical-maps/
|
||||
```
|
||||
2. The server auto-detects new files within 5 seconds
|
||||
3. Verify via `curl http://your-server:8090/maps`
|
||||
|
||||
## Step 4: Client Downloads
|
||||
|
||||
The client uses `MapDownloader` (autoload at `client/scripts/map_downloader.gd`)
|
||||
to fetch and load maps at runtime.
|
||||
|
||||
### How It Works
|
||||
|
||||
1. Player joins a server that specifies a map name
|
||||
2. The client checks its local cache (`user://maps/manifest.json`)
|
||||
3. If the map is not cached, `MapDownloader.fetch_map_list()` polls
|
||||
the registry server to verify the map exists
|
||||
4. `MapDownloader.download_map(name)` downloads the `.pck` to `user://maps/`
|
||||
5. `ProjectSettings.load_resource_pack()` loads it into the game
|
||||
6. The server changes to the new level
|
||||
|
||||
### Map Caching
|
||||
|
||||
```
|
||||
user://maps/
|
||||
├── de_dust2.pck # Cached map binary
|
||||
├── de_inferno.pck # Another cached map
|
||||
└── manifest.json # Cache index
|
||||
```
|
||||
|
||||
**Cache behaviour:**
|
||||
- Maps are cached to disk after download (survives restarts)
|
||||
- The manifest tracks version numbers for staleness checks
|
||||
- `MapDownloader.remove_map(name)` clears a specific map
|
||||
- `MapDownloader.clear_cache()` removes all cached maps
|
||||
|
||||
### Configuration
|
||||
|
||||
The `MapDownloader` autoload can be configured in-game:
|
||||
|
||||
```gdscript
|
||||
# Set the registry server URL (override default http://127.0.0.1:8090)
|
||||
MapDownloader.registry_url = "http://maps.myserver.com:8090"
|
||||
```
|
||||
|
||||
The URL can also be set via the `MAP_REGISTRY_URL` environment variable,
|
||||
which is read in `_ready()`.
|
||||
|
||||
### Signals
|
||||
|
||||
```gdscript
|
||||
# Register for map lifecycle events
|
||||
MapDownloader.map_list_loaded.connect(_on_map_list)
|
||||
MapDownloader.map_download_progress.connect(_on_download_progress)
|
||||
MapDownloader.map_download_complete.connect(_on_download_done)
|
||||
MapDownloader.map_loaded.connect(_on_map_loaded)
|
||||
```
|
||||
|
||||
## Full Pipeline Example
|
||||
|
||||
```bash
|
||||
# 1. Package from the template project
|
||||
cd maps/my_first_map
|
||||
godot --headless --script ../../scripts/map_packaging/pack_map.gd \
|
||||
--map=res://de_my_map.tscn
|
||||
|
||||
# 2. Start registry server
|
||||
cd ../..
|
||||
python3 scripts/map_packaging/map_registry_server.py --port 8090 &
|
||||
|
||||
# 3. Deploy map
|
||||
cp ~/.local/share/godot/app_userdata/MapTemplate/packed_maps/de_my_map.pck \
|
||||
scripts/map_packaging/packed_maps/
|
||||
cp ~/.local/share/godot/app_userdata/MapTemplate/packed_maps/de_my_map.json \
|
||||
scripts/map_packaging/packed_maps/
|
||||
|
||||
# 4. Verify
|
||||
curl -s http://localhost:8090/maps | python3 -m json.tool
|
||||
|
||||
# 5. In-game: connect to server and play the map
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [FAQ & Troubleshooting](06-faq-and-troubleshooting.md) — common issues
|
||||
@@ -0,0 +1,238 @@
|
||||
# FAQ & Troubleshooting
|
||||
|
||||
Common questions, issues, and solutions encountered while building maps.
|
||||
|
||||
## Template Project
|
||||
|
||||
### Q: Can I use the template project without the main game repo?
|
||||
|
||||
Yes. The template project at `client/map_template/` is a **standalone Godot
|
||||
project**. Copy it anywhere, open it in Godot, and start building. You only
|
||||
need the main repo for packaging (`pack_map.gd`) and the registry server
|
||||
(`map_registry_server.py`).
|
||||
|
||||
### Q: Why does Godot say "Import" instead of "Open"?
|
||||
|
||||
The template has its own `project.godot`, making it a separate project. Use
|
||||
**Import** to add it to your Godot project list. You're not opening a scene
|
||||
within an existing project.
|
||||
|
||||
### Q: How do I add the main game's assets to my map?
|
||||
|
||||
The template uses placeholder materials. For final maps:
|
||||
|
||||
1. Copy your map's `.tscn` into the main project under `client/maps/<name>/`
|
||||
2. Open it from the main project's scenes tab
|
||||
3. The main game's modular kit and PBR materials will be available
|
||||
4. Assign materials to your CSG/MeshInstance3D nodes from the main project's
|
||||
asset library
|
||||
|
||||
### Q: Can I use external models from Blender/Maya?
|
||||
|
||||
Yes. Export as `.glb` or `.obj` and import into Godot. Keep triangle counts
|
||||
within budget (≤ 50K total, ≤ 5K per mesh recommended). Place imported meshes
|
||||
as children of the map scene root.
|
||||
|
||||
## Geometry & CSG
|
||||
|
||||
### Q: My map has light leaks at the corners
|
||||
|
||||
Thin gaps between CSG blocks let light bleed through. Fix by overlapping CSG
|
||||
edges by **0.1 units** instead of butting them flush.
|
||||
|
||||
```
|
||||
Bad: Wall ends at x=10.0, floor ends at x=10.0 → gap
|
||||
Good: Wall extends to x=10.1, floor ends at x=10.0 → overlap
|
||||
```
|
||||
|
||||
### Q: Players can walk through walls
|
||||
|
||||
Make sure `use_collision = true` is set on all CSG geometry nodes. CSG nodes
|
||||
don't have collision by default — you must enable it.
|
||||
|
||||
### Q: My map renders as a grey box / no materials
|
||||
|
||||
Placeholder materials are normal during construction. For the final look, you
|
||||
need to:
|
||||
1. Copy the map to the main project (see above)
|
||||
2. Assign materials from the game's asset library
|
||||
3. Re-bake LightmapGI after materials are assigned
|
||||
|
||||
### Q: CSG performance is bad in the editor
|
||||
|
||||
This is normal. CSG is re-calculated every time you move a node. For testing,
|
||||
convert stable geometry to MeshInstance3D:
|
||||
- Right-click CSG parent → **Convert CSG to Mesh**
|
||||
- Keep a backup copy of the CSG hierarchy in another scene
|
||||
|
||||
### Q: How do I make a skybox?
|
||||
|
||||
Add a `WorldEnvironment` node and assign an Environment resource:
|
||||
1. Set **Background** mode to **Sky**
|
||||
2. Create a new **Sky** resource
|
||||
3. Assign a **PanoramaSky** or **ProceduralSky** material
|
||||
4. The sky appears behind your geometry
|
||||
|
||||
### Q: My map has no collision after CSG → Mesh conversion
|
||||
|
||||
When you convert CSG to MeshInstance3D, collision is not preserved. Add a
|
||||
`CollisionShape3D` as a child of each MeshInstance3D, or use Godot's
|
||||
**Mesh → Create Trimesh Collision** (right-click the MeshInstance3D).
|
||||
|
||||
## Lighting
|
||||
|
||||
### Q: My lightmap bake takes forever
|
||||
|
||||
Baking time depends on:
|
||||
- **texel_scale** — lower = higher resolution = longer bake (try 2.0 for testing)
|
||||
- **bounces** — fewer bounces = faster bake (use 1 for quick tests, 3 for final)
|
||||
- **Map size** — larger maps bake longer
|
||||
- **Light count** — more lights = more calculations
|
||||
|
||||
For quick iteration, use Low quality (0) during development and High quality (2)
|
||||
for the final bake.
|
||||
|
||||
### Q: Baked lighting is too dark
|
||||
|
||||
- Increase `bounces` to 3 (more light bounce = brighter interiors)
|
||||
- Add a low-intensity ambient fill (OmniLight3D with Static bake mode)
|
||||
- Lower the `texel_scale` for more lightmap resolution
|
||||
- Check that the DirectionalLight3D intensity is sufficient
|
||||
|
||||
### Q: Lightmap has visible seams
|
||||
|
||||
Seams appear where lightmap texels don't align. Fixes:
|
||||
- Lower `texel_scale` to 1.0 (higher resolution lightmaps)
|
||||
- Ensure CSG edges overlap (no gaps)
|
||||
- Convert CSG to MeshInstance3D before final bake
|
||||
|
||||
### Q: Shadows look blocky or jagged
|
||||
|
||||
This is Godot's shadow mapping at default resolution. For competitive clarity:
|
||||
- Keep `shadow_bias` at 0.1 (higher = softer shadows, fewer artefacts)
|
||||
- Directional shadow `max_distance` at 20–30 (shadows beyond this use lower
|
||||
resolution)
|
||||
|
||||
### Q: I see a sphere artifact on my ReflectionProbe
|
||||
|
||||
The ReflectionProbe has a `probe_mode` property — if set to **Reflection Probe**
|
||||
(sphere mode), it renders as a visible sphere in reflections. Change to **Box
|
||||
Probe** for interior spaces, or disable reflection visibility via the `visible`
|
||||
property on the probe's MeshInstance3D child (Godot 4.x).
|
||||
|
||||
### Q: Can I have a day/night cycle?
|
||||
|
||||
No. LightmapGI bakes lighting statically. Dynamic lighting is limited to
|
||||
≤ 4 real-time lights. Day/night cycles are outside the current scope.
|
||||
|
||||
## Validation
|
||||
|
||||
### Q: The validator says "No MeshInstance3D nodes found"
|
||||
|
||||
Your map is using CSG nodes that haven't been converted to meshes. The
|
||||
validator counts mesh triangles — CSG nodes are scanned as MeshInstance3D
|
||||
after conversion. Either:
|
||||
1. Convert CSG → Mesh for the area you want validated (right-click → Convert CSG to Mesh)
|
||||
2. This warning is expected during early construction stages
|
||||
|
||||
### Q: The validator fails with "Scene cannot be instantiated"
|
||||
|
||||
The scene depends on resources that don't exist in the template project. This
|
||||
usually happens when the map references game assets from the main project.
|
||||
Either:
|
||||
1. Run the validator from the **main game project** (`--path client` with the
|
||||
main `client/project.godot`), or
|
||||
2. Strip external dependencies — the map should be self-contained
|
||||
|
||||
### Q: The validator reports texture errors but my textures are 1024×1024
|
||||
|
||||
Check for textures with different aspect ratios. A 2048×512 texture fails
|
||||
because the larger dimension (2048) exceeds 1024. Resize or tile.
|
||||
|
||||
### Q: Can I skip certain validator checks?
|
||||
|
||||
Not directly. Run the validator, fix errors, then fix warnings. If a check
|
||||
produces a false positive, the `@tool` scripts are in
|
||||
`client/tools/validate_map/` — you can edit the constants (e.g.
|
||||
`MAX_TOTAL_TRIANGLES`) for custom pipelines.
|
||||
|
||||
## Packaging
|
||||
|
||||
### Q: The .pck file is very large
|
||||
|
||||
The `.pck` includes all dependencies of the scene. Large files mean:
|
||||
- High-resolution textures (use ≤ 1024×1024 per the budget)
|
||||
- High-poly meshes (stay under 50K triangles)
|
||||
- Embedded audio or other large assets
|
||||
|
||||
Run the validator first to check budgets.
|
||||
|
||||
### Q: I get "Failed to load pack" in-game
|
||||
|
||||
Possible causes:
|
||||
- `.pck` file is corrupted (check SHA-256 checksum)
|
||||
- The map scene or a dependency path changed between packaging and loading
|
||||
- The `.pck` was built with a different Godot version than the game
|
||||
- The resource path in the `.pck` conflicts with a path in the base game
|
||||
|
||||
**Solution:** Re-package the map from the same Godot version the game uses.
|
||||
Verify with `curl` that the download matches the server's checksum.
|
||||
|
||||
### Q: Can I update a map without changing the client version?
|
||||
|
||||
Yes. The `.pck` is additive content. Update the `.pck` on the registry server
|
||||
and increment the `version` field in the `.json` metadata. The client will
|
||||
re-download when it notices the version mismatch.
|
||||
|
||||
### Q: Can I host the registry server on a different port?
|
||||
|
||||
Yes. Use `--port <port>` or the `MAP_REGISTRY_PORT` environment variable.
|
||||
The client's `MapDownloader` has a `registry_url` property you can set.
|
||||
|
||||
### Q: Can I load more than one .pck at a time?
|
||||
|
||||
Yes. `ProjectSettings.load_resource_pack()` is additive. Multiple `.pck` files
|
||||
can be loaded simultaneously — each adds its resources to the global namespace.
|
||||
|
||||
### Q: The registry server returns 404 for my map
|
||||
|
||||
- Check that the `.pck` file is in the maps directory (default `./packed_maps/`)
|
||||
- Check the file extension is `.pck` (lowercase)
|
||||
- The server auto-scans every 5 seconds — wait or send SIGHUP to force a scan
|
||||
- Verify by listing: `curl http://localhost:8090/maps`
|
||||
|
||||
## General
|
||||
|
||||
### Q: Can I make a hostage rescue map?
|
||||
|
||||
The SDK is gameplay-agnostic. The groups `ct_spawn`, `t_spawn`, `bomb_site`,
|
||||
and `buy_zone` are the default set. You can add your own groups (e.g.,
|
||||
`hostage`, `hostage_zone`) — the game logic discovers them at runtime.
|
||||
Check the game's plugin API documentation for custom group support.
|
||||
|
||||
### Q: Can I use this SDK for a different game?
|
||||
|
||||
The SDK is designed for Tactical Shooter's Godot project structure. The
|
||||
packaging and validation scripts are reusable for any Godot map-authoring
|
||||
workflow, but the group conventions and gameplay nodes are game-specific.
|
||||
|
||||
### Q: The editor validator doesn't run
|
||||
|
||||
The `template_map.gd` script uses `@tool` and checks `Engine.is_editor_hint()`.
|
||||
Make sure:
|
||||
1. The script is attached to the root node of the scene
|
||||
2. You're editing the scene in the Godot editor (not running the game)
|
||||
3. The Output panel is visible (Window → Output)
|
||||
|
||||
### Q: How do I report a bug in the SDK?
|
||||
|
||||
Open an issue on the project's Gitea repository. Include:
|
||||
- SDK version (commit hash)
|
||||
- Godot version
|
||||
- Steps to reproduce
|
||||
- Log output (editor or CLI validator)
|
||||
|
||||
### Q: Can I contribute validator modules?
|
||||
|
||||
Yes. See [Validation — Adding a Custom Validator Module](04-validation.md#adding-a-custom-validator-module).
|
||||
Submit a pull request with your module and updated documentation.
|
||||
Reference in New Issue
Block a user