43. Edit Mode First Pass
In order to author content in an easier fashion, we'll add a simple "Edit Mode". In Edit Mode, we can fly around the scene and click somewhere to select a point in 3D space.
The reason this is a First Pass, is because after using the tool, it's clear that we can do better with some consideration--more on that later.
It's important to show the first pass rather than skip straight to the "better" version; so you can judge for yourself which workflow you prefer.
Keeping track of just a few pieces of state will suffice for this tool:
// new file: game/editor.odin
package main
import "vendor:raylib"
EDITOR_MOVESPEED :: 3
EDITOR_SENSITIVITY :: 0.003
// Add instance of this to Game_State as 'editor'
Editor_State :: struct {
is_active: bool,
free_mouse: bool,
selected_point: Vec3,
}
We're going to have a fly-mode in which the camera is a first person and we can fly around. When free_mouse is false then we can no longer move the camera with the mouse, but we get access to the cursor so we can select a point.
Before we create the update and render procedures for the editor, let's update the Input_State to record when our keys are released:
// Add these fields to Input_State
debug_edit_mode_released: bool,
debug_toggle_cursor_lock_released: bool,
Now in input_update, we'll update these values:
is.debug_edit_mode_released = rl.IsKeyReleased(.F5)
is.debug_toggle_cursor_lock_released = rl.IsKeyReleased(.F)
Each frame in which the editor is active, we call editor_update:
// in game_update:
// Update the last argument to console_handle_input
// to take the selected_point rather than the
// player's position
if gs.console.is_active {
if !console_handle_input(&gs.console, gs.scene.scene_name, gs.editor.selected_point) {
return
}
}
if gs.input.debug_edit_mode_released {
gs.editor.is_active = !gs.editor.is_active
if gs.editor.is_active {
rl.HideCursor()
}
}
if gs.editor.is_active {
editor_update(
&gs.editor, // update free mouse
gs.input, // needs to know inputs
&gs.rendering.camera, // fly-camera
gs.assets.models[gs.scene.scene_name], // raycast
gs.time.delta, // fly-camera
)
return
}
The editor update procedure requires access to some relevant state, some read-only and some passed by pointer for mutation.
editor_update :: proc(
es: ^Editor_State,
is: Input_State,
camera: ^raylib.Camera,
model: raylib.Model,
dt: f32,
) {
if is.debug_toggle_cursor_lock_released {
es.free_mouse = !es.free_mouse
if es.free_mouse {
rl.EnableCursor()
} else {
rl.DisableCursor()
}
}
if es.free_mouse {
if is.clicking {
es.selected_point = max(f32)
// fire ray into scene geometry
// if hit, selection is set
ray := rl.GetScreenToWorldRay(is.cursor, camera^)
for i in 0 ..< model.meshCount {
hit := rl.GetRayCollisionMesh(ray, model.meshes[i], raylib.Matrix(1))
if hit.hit {
es.selected_point = hit.point
break
}
}
}
} else {
rl.CameraYaw(camera, -is.move.x * EDITOR_SENSITIVITY, false)
rl.CameraPitch(camera, -is.move.y * EDITOR_SENSITIVITY, true, false, false)
}
forward := rl.GetCameraForward(camera)
right := rl.GetCameraRight(camera)
up := rl.GetCameraUp(camera)
move: Vec3
if is.up do move += forward
if is.down do move -= forward
if is.left do move -= right
if is.right do move += right
if is.debug_hoist do move += up
if is.debug_lower do move -= up
if rl.Vector3Length(move) > 0 {
move = rl.Vector3Normalize(move) * EDITOR_MOVESPEED * dt
camera.position += move
camera.target += move
}
}
Here's the entire game_render procedure. We draw the edit mode before the console so the console shows up on top:
@(export)
game_render :: proc(gs: ^Game_State) {
switch gs.scene_kind {
case .MAIN_MENU:
render_main_menu()
case .FIELD, .BATTLE:
render_field()
}
// new
if gs.editor.is_active {
editor_render(&gs.editor, gs.rendering.camera, gs.assets.models[gs.scene.scene_name])
}
if gs.console.is_active {
console_render(&gs.console, gs.window_width)
debug_text_flush()
}
}
And the editor_render procedure itself is straightforward:
editor_render :: proc(es: ^Editor_State, camera: raylib.Camera, model: raylib.Model) {
// Clear what's already rendered and produce
// a wireframe view of the scene
rl.ClearBackground(rl.BLACK)
rl.BeginMode3D(camera)
rl.DrawModelWires(model, {}, 1, rl.WHITE)
rl.DrawSphereWires(es.selected_point, 0.2, 5, 5, rl.YELLOW)
rl.EndMode3D()
}
That's all for the first pass of the editor.
Try it out, see how it feels.
The current procedure is:
- F5 to start Edit Mode.
- F to enable the cursor.
- Click somewhere to move the selected point.
- Grave/Tilde to open the console.
- Type
door DoorNameto spawn a door. - Type
load flatto reload the scene. - Door should be visible as blue bounding box.
- If you want to link, same as before.
After using this a few times, you may come to realise what I did. It doesn't feel great.
It's both imprecise with the placement and unwieldy with the console commands.
In the upcoming lessons, we'll take a step back, ask ourselves what our specific game needs and then produce custom tools tailored specifically for our JRPG.