Cinematic Camera

This paper plugin allows you to create movie level cinematic cameras and make a player follow your path using these

46

Cinematic Camera

🎬 CinematicCamera Plugin — Full README

> A Minecraft (Paper/Spigot) plugin for creating smooth, cinematic camera paths with rotation, speed zones, keyframe effects, and more.

---

Table of Contents

1. Quick Start 2. How It Works — Core Architecture 3. Commands Reference 4. Removing a Keyframe (removekeyframe) 5. Keyframe Types — Full Reference 6. Speed Zones 7. Auto-Play Trigger 8. Playback System — How It Works in Code 9. Path Interpolation — Catmull-Rom Splines 10. Camera Rotation Interpolation 11. State Management Maps 12. Full Workflow Example 13. Tab Completion 14. showcase

---

Quick Start

1. Run `/cinematicwand` to get the wand item (a Blaze Rod). 2. Walk to a position and Left Click to add a waypoint. Your camera's yaw (horizontal rotation) and pitch (vertical rotation) are saved with each waypoint. 3. Add at least 2+ waypoints along your desired path. 4. Run `/cinematic save mypath normal` to save it. 5. Run `/cinematic play mypath` to watch it.

---

How It Works — Core Architecture

The plugin is a single class `CinematicCamera` that extends `JavaPlugin` and implements `Listener`. All state is stored in in-memory `HashMap`/`HashSet` fields (no persistence to disk between restarts).

Core State Maps

| Field | Type | Purpose | |-------|------|---------| | `paths` | `Map<String, CinematicPath>` | All saved cinematic paths, keyed by name | | `editors` | `Map<UUID, PathEditor>` | Active wand sessions per player — holds their unsaved waypoints | | `playingCinematic` | `Set<UUID>` | Players currently watching a cinematic | | `originalGameModes` | `Map<UUID, GameMode>` | Stores each player's gamemode before the cinematic starts so it can be restored | | `activeAnimations` | `Map<UUID, BukkitTask>` | The running `BukkitRunnable` task for each player — used to cancel on stop | | `savedLocations` | `Map<UUID, Location>` | Player's location before cinematic — teleported back on end | | `savedFlying` | `Map<UUID, Boolean>` | Whether the player was flying before — restored on end | | `awaitingClick` | `Set<UUID>` | Players currently paused on a `PAUSE_CLICK` keyframe, waiting for any click |

---

Commands Reference

`/cinematicwand`

Gives you the Blaze Rod wand item. - Shift + Right Click (air or block): Adds your current position + rotation as a waypoint - Shift + Right Click: Shows current waypoint count and how to save - Shift + Right Click: Clears all your current unsaved waypoints

`/cinematic save <name> <speed>`

Saves your current waypoints as a named path.

Speed values: | Input | Multiplier | |-------|-----------| | `slow` or `cinematic` | 0.3x | | `normal` or `medium` | 1.0x | | `fast` | 2.0x | | `veryfast` or `rapid` | 3.5x | | `0.1` – `5.0` | Custom |

`/cinematic play <name>`

Starts the cinematic for you. Puts you in SPECTATOR mode, applies invisibility, teleports you to waypoint 0, and begins the animation loop.

`/cinematic stop`

Immediately stops the cinematic, cancels the task, removes effects, and teleports you back to your saved location.

`/cinematic list`

Lists all saved paths with their waypoint count, speed, keyframe count, speed zone count, and auto-play status.

`/cinematic delete <name>`

Removes a saved path entirely.

`/cinematic setspeed <name> <speed>`

Changes the base speed multiplier of an existing path.

`/cinematic toggle <name>`

Toggles the auto-play trigger on or off for a path. When on, walking within 1.5 blocks of the first waypoint auto-starts the cinematic.

`/cinematic clear`

Clears your current unsaved wand waypoints (same as Shift+Right Click).

`/cinematic settitle <name> <title> [| subtitle]`

Sets the title shown at the beginning of a cinematic. - Use `|` to separate title and subtitle: `/cinematic settitle mypath Welcome | Enjoy the view`

`/cinematic addkeyframe <name> <waypointIndex> <type> [args...]`

Adds an effect keyframe that triggers when the camera reaches the given waypoint.

`/cinematic listkeyframes <name>`

Lists all keyframes on a path with their index, waypoint, type, and text preview.

`/cinematic removekeyframe <name> <index>`

Removes a keyframe by its list index (shown in `/cinematic listkeyframes`).

`/cinematic addzone <name> <startWP> <endWP> <speed>`

Adds a speed zone between two waypoint indices. Overrides the base speed for that segment.

`/cinematic listzones <name>`

Lists all speed zones on a path.

`/cinematic removezone <name> <index>`

Removes a speed zone by its list index.

---

Removing a Keyframe

This is a 2-step process:

Step 1 — Find the keyframe index: ``` /cinematic listkeyframes spawn ``` This shows output like: ``` ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ [0] wp#2 text_title → Welcome [1] wp#4 fade_black [2] wp#6 sound → ENTITY_WITHER... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ```

Step 2 — Remove by that index number: ``` /cinematic removekeyframe spawn 1 ``` This removes the `fadeblack` at index `[1]`.

> ⚠️ Indexes are positional (0, 1, 2...) and shift after a removal. Always re-run `listkeyframes` after removing one if you plan to remove more.

---

Keyframe Types — Full Reference

Add any of these with `/cinematic addkeyframe <path> <waypointIndex> <type> [args]`

---

`title`

Shows a title on screen. ``` /cinematic addkeyframe mypath 0 title 10 60 20 Welcome to the Tour | Enjoy the view ``` Args: `<fadeIn ticks> <stay ticks> <fadeOut ticks> <title text> [| subtitle]`

How it works in code: Calls `player.showTitle()` using the Adventure API `Title.title()` with `Title.Times`, constructing bold white title text and gray subtitle text.

---

`actionbar`

Shows text in the action bar (above the hotbar). ``` /cinematic addkeyframe mypath 2 actionbar Look to your left! ```

How it works: Calls `player.sendActionBar()` with yellow-tinted text.

---

`chat`

Sends a ✦ prefixed message to the player's chat. ``` /cinematic addkeyframe mypath 3 chat This is where the story began. ```

---

`pause`

Pauses the camera movement for a set number of seconds. ``` /cinematic addkeyframe mypath 4 pause 3 ``` Args: `<seconds>`

How it works: Sets `paused = true` and `pauseTicksLeft = seconds * 20`. The `BukkitRunnable` returns early every tick until the counter hits 0.

---

`click`

Pauses the camera indefinitely until the player clicks (any mouse button). ``` /cinematic addkeyframe mypath 5 click ```

How it works: Adds the player to `awaitingClick` set and shows an action bar prompt. The `PlayerInteractEvent` handler removes them from the set on any click. The animation loop checks `awaitingClick.contains(uuid)` and returns early until cleared.

---

`sound`

Plays a sound at the player's location. ``` /cinematic addkeyframe mypath 6 sound ENTITY_WITHER_SPAWN 1.0 0.8 ``` Args: `<SOUND_NAME> [volume] [pitch]`

How it works: Uses `player.playSound()`. Sound name is looked up via `Registry.SOUNDS.get(NamespacedKey.minecraft(...))`. Falls back to `BLOCK_NOTE_BLOCK_PLING` if invalid.

---

`particle`

Spawns a burst of firework particles around the camera. ``` /cinematic addkeyframe mypath 7 particle ```

How it works: Calls `world.spawnParticle(Particle.FIREWORK, location, 40, 1, 1, 1, 0.05)`.

---

`shake`

Shakes the camera by rapidly teleporting it in tiny random offsets. ``` /cinematic addkeyframe mypath 8 shake ```

How it works: Schedules 10 `runTaskLater` calls (1 tick apart) that each add a small random offset (`-0.1` to `+0.1` on each axis) to the player's location and teleport them there.

---

`fadeblack`

Applies Blindness effect to fade the screen to black. ``` /cinematic addkeyframe mypath 9 fadeblack 2 ``` Args: `<duration seconds>` (default: 2)

How it works: Applies `PotionEffectType.BLINDNESS` for `duration * 20` ticks.

---

`fadewhite`

Applies Glowing effect to create a white flash/fade. ``` /cinematic addkeyframe mypath 10 fadewhite 2 ``` Args: `<duration seconds>` (default: 2)

How it works: Applies `PotionEffectType.GLOWING` for `duration * 20` ticks.

---

`lightning`

Strikes a visual-only lightning bolt at the camera position. ``` /cinematic addkeyframe mypath 11 lightning ```

How it works: Calls `world.strikeLightningEffect(location)` — this is the visual-only version (no fire, no damage).

---

`explosion`

Creates a visual explosion effect at the camera position. ``` /cinematic addkeyframe mypath 12 explosion ```

How it works: Calls `world.createExplosion(location, 0, false, false)` — power 0 means no block damage, no fire.

---

`time`

Sets the world time instantly. ``` /cinematic addkeyframe mypath 3 time 6000 ``` Args: `<0–24000>` (0 = sunrise, 6000 = noon, 12000 = sunset, 18000 = midnight)

How it works: Calls `world.setTime((long) kf.duration)`.

---

`weather`

Changes the world's weather. ``` /cinematic addkeyframe mypath 5 weather rain /cinematic addkeyframe mypath 8 weather clear /cinematic addkeyframe mypath 10 weather thunder ``` Args: `clear | rain | thunder`

How it works: - `clear` → `world.setStorm(false)` - `rain` → `world.setStorm(true)` - `thunder` → `world.setStorm(true)` + `world.setThundering(true)`

---

Speed Zones

Speed zones override the base path speed for a specific range of waypoints, letting you slow down for dramatic moments or fast-forward through boring stretches.

``` /cinematic addzone mypath 0 3 0.3 ``` This makes waypoints 0–3 play at 0.3x speed (very slow/cinematic).

``` /cinematic addzone mypath 5 9 3.0 ``` This makes waypoints 5–9 play at 3x speed.

How it works in code: During `generateSmoothPath()`, each segment `i` checks `speedZones` to see if `i >= zone.startWaypoint && i < zone.endWaypoint`. If a match is found, `pointsInSegment = basePoints / segmentSpeed`. A higher speed multiplier means fewer interpolated points between waypoints, so they're traversed faster. Each `CameraFrame` also stores `speedMultiplier` which the animation loop uses to skip frames (`index += (1 + skip)` where `skip = speedMultiplier - 1`).

---

Auto-Play Trigger

When auto-play is enabled on a path (it's on by default when you save), any player who walks within 1.5 blocks of the path's first waypoint will automatically start the cinematic.

- Toggle it: `/cinematic toggle mypath` - Checked in `PlayerMoveEvent` — only triggers if the player is not already in a cinematic

---

Playback System — How It Works in Code

When `playCinematic(player, path)` is called:

1. The player is added to `playingCinematic`, their gamemode/location/flying state are saved. 2. They're set to `SPECTATOR` mode with infinite `INVISIBILITY`. 3. They're teleported to `waypoints.get(0)`. 4. `path.generateSmoothPath()` runs and produces a `List<CameraFrame>`. 5. A `Map<Integer, Keyframe>` is built that maps each keyframe to its smooth-path index (`waypointIndex * basePointsPerSegment`). 6. A `BukkitRunnable` is scheduled to run every tick (1/20th of a second).

Inside the animation loop (per tick):

``` tick fires → if player offline: endCinematic, cancel → if timed pause active: decrement counter, return → if awaiting click: return → if index >= smoothPath.size(): endCinematic, send message, cancel → check kfMap for keyframe at current index, apply it → if PAUSE_TIMED: set paused=true, set counter, increment index, return → if PAUSE_CLICK: add to awaitingClick, increment index, return → teleport player to smoothPath.get(index).location (position + yaw/pitch) → spawn ambient END_ROD particle every 10 ticks → advance index (by 1, or more if speed zone skip > 1) ```

The `endCinematic` method: - Removes player from all state maps - Cancels the BukkitTask - Removes potion effects (Invisibility, Blindness, Glowing) - Clears title - Restores original GameMode - Teleports player back to their saved location - Restores flying state

---

Path Interpolation — Catmull-Rom Splines

Raw waypoints are spaced far apart. To get smooth camera movement, the plugin interpolates between them using Catmull-Rom splines — a curve that passes *through* each control point (unlike Bezier curves which are pulled toward them).

For each segment from waypoint `i` to `i+1`, 4 control points are used: - `p0` = previous waypoint (or `p1` if at start) - `p1` = current waypoint - `p2` = next waypoint - `p3` = waypoint after next (or `p2` if at end)

The formula applied per axis (x, y, z): ``` x(t) = 0.5 * ( 2*p1 + (-p0 + p2) * t + (2*p0 - 5*p1 + 4*p2 - p3) * t² + (-p0 + 3*p1 - 3*p2 + p3) * t³ ) ```

Where `t` goes from 0.0 to 1.0 across the segment. The number of points per segment is: ``` basePointsPerSegment = max(10, 60 / speed) ``` So at `normal` speed (1.0x): 60 points per segment = 60 ticks = 3 seconds per waypoint-to-waypoint. At `fast` (2.0x): 30 points = 1.5 seconds.

---

Camera Rotation Interpolation

Each waypoint stores the player's pitch (horizontal, 0°–360°) and pitch (vertical, -90° to +90°) at the time it was placed.

When the wand records a waypoint: ```java Location loc = player.getLocation().clone(); // includes yaw and pitch editor.addWaypoint(loc); ```

During smooth path generation, rotation is linearly interpolated between adjacent waypoints:

Pitch uses standard linear interpolation: ```java float pitch = (float) lerp(p1.getPitch(), p2.getPitch(), t); // lerp(a, b, t) = a + (b - a) * t ```

Yaw uses angle-aware lerp to prevent spinning the wrong way around (e.g., 350° → 10° goes forward, not backward 340°): ```java double lerpAngle(double a, double b, double t) { double diff = ((b - a + 180) % 360) - 180; return a + diff * t; } ``` This normalizes the difference to the range `-180` to `+180`, ensuring the rotation always takes the shortest path.

---

State Management Maps

Why SPECTATOR mode?

Players in SPECTATOR can be freely teleported each tick without physics interference. `PlayerMoveEvent` blocks X/Y/Z movement (while allowing head rotation) so the player can't fight the cinematic.

Why save/restore gamemode and location?

When the cinematic ends (normally or via `/cinematic stop`), the player needs to return to exactly where they were with exactly the gamemode they had. These are stored in `originalGameModes` and `savedLocations` at start and retrieved in `endCinematic`.

Why the `awaitingClick` set?

The animation loop runs every tick independently of player input. The `PAUSE_CLICK` keyframe needs to pause across ticks until a click happens. The set acts as a shared flag: the animation loop checks it, and `PlayerInteractEvent` clears it.

---

Full Workflow Example

Here's a complete example building a dramatic cinematic flythrough:

```

1. Get wand

/cinematicwand

2. Walk to starting position, look where you want the camera to face, Left Click

Add 5+ more waypoints around your scene

3. Save at slow speed

/cinematic save myflythrough slow

4. Add an intro title at waypoint 0

/cinematic addkeyframe myflythrough 0 title 10 80 20 Welcome | The story begins here

5. Play a dramatic sound at waypoint 2

/cinematic addkeyframe myflythrough 2 sound ENTITY_WITHER_SPAWN 1.0 0.6

6. Fade to black at waypoint 4

/cinematic addkeyframe myflythrough 4 fadeblack 3

7. Set time to sunset at waypoint 5

/cinematic addkeyframe myflythrough 5 time 12000

8. Pause the camera at waypoint 3 for 2 seconds

/cinematic addkeyframe myflythrough 3 pause 2

9. Make waypoints 1-2 slower (dramatic pan)

/cinematic addzone myflythrough 1 2 0.2

10. Make waypoints 5-7 faster (fast flythrough)

/cinematic addzone myflythrough 5 7 3.0

11. Set intro title for the path

/cinematic settitle myflythrough The Great Journey | Act I

12. Review everything

/cinematic listkeyframes myflythrough /cinematic listzones myflythrough

13. Play it

/cinematic play myflythrough

14. If you need to remove keyframe [1] (fadeblack):

/cinematic listkeyframes myflythrough

→ finds index, e.g. [1]

/cinematic removekeyframe myflythrough 1 ```

---

Tab Completion

The plugin provides tab completion on `/cinematic`: - Arg 1: all subcommand names - Arg 2 (for path commands): all saved path names - Arg 3 (for `save`/`setspeed`/`addzone`): speed presets (`slow`, `normal`, `fast`, `veryfast`, `0.5`, `1.0`, etc.) - Arg 3 (for `addkeyframe`): waypoint index suggestions (`0`–`5`) - Arg 4 (for `addkeyframe`): all keyframe types - Arg 5 (for `addkeyframe weather`): `clear`, `rain`, `thunder`

ADS