Cinematic Camera
This paper plugin allows you to create movie level cinematic cameras and make a player follow your path using these
Quick challenge
How far can you run before the mobs catch you?
Minecraft check
Confirm your run
Complete the quick check to get your code.
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`