- What it does
- Wraps low-level packets like
CameraInstructionPacketandCameraPresetsPacketbehind a fluent PHP API. - Manages per-player
CameraSessionobjects and exposes builder-style helpers for sending camera instructions.
- Wraps low-level packets like
- Why it exists
- Bedrock Creator Cameras are powerful but difficult to use directly: packet structures are complex and preset/timeline management is verbose.
- This plugin hides that complexity behind simple method chaining and a timeline DSL.
- Key concepts
Camera::of(Player)– main entry point to obtain a camera session for a player.CameraSession– per-player object that manages camera state and scheduled tasks.- Builders –
CameraSetBuilder,CameraFadeBuilder,CameraTargetBuilder,CameraFovBuilder,CameraFogBuilder,CameraSplineBuilder(experimental / discouraged). CameraTimeline– utility to queue and play camera instructions over time (cutscenes).CameraPresetRegistry/CameraPresetBuilder– registration and synchronization of camera presets with the client.AimAssistPresetRegistry– registration and synchronization of aim‑assist categories and presets with the client.CameraMarker– in-world helper entity (fake player) to place and preview camera viewpoints; supports interact and attack callbacks.
- Server
- PocketMine-MP
5.0.0or higher.
- PocketMine-MP
- Installation
- Clone this repository or place the
CameraAPIplugin folder into your PMMP server'spluginsdirectory. - Restart the server; the plugin will load automatically according to
plugin.yml.
- Clone this repository or place the
- How it works at runtime
- The
Mainplugin class is loaded and:- On
PlayerJoinEvent,CameraSessionManager::createSession($player)creates a session. CameraPresetRegistry::sendTo($player)sends camera presets to the client.AimAssistPresetRegistry::sendTo($player)sends aim‑assist presets and categories to the client.
- On
- When
StartGamePacketorResourcePackStackPacketis sent, the plugin automatically enables theexperimental_creator_camerasexperiment flag.
- The
use kim\present\cameraapi\Camera;
use pocketmine\player\Player;
function example(Player $player) : void{
// Get camera session for this player
$session = Camera::of($player);
// Fade to black for 1s, stay 1s, fade out for 1s
$session->fade()
->in(1.0)
->stay(1.0)
->out(1.0)
->send();
}Camera::of(Player $player) : CameraSession- Description: Returns the
CameraSessionfor the given player, creating it if necessary. - Parameters
Player $player– target player whose camera will be controlled.
- Returns
CameraSession– session object managing camera state and commands for the player.
- Example
$session = Camera::of($player);
$session->clear(); // Reset camera to default- Timeline helpers
Camera::timeline() : CameraTimeline
Camera::loadTimeline(string $json) : CameraTimelinetimeline()creates an empty, code-driven timeline.loadTimeline()builds a timeline from a JSON description usingCameraTimelineParser.
The session keeps camera context per player and provides builders and utility methods.
- Method summary
getPlayer() : ?Playerset() : CameraSetBuilderfade() : CameraFadeBuildertarget() : CameraTargetBuilderfov() : CameraFovBuilderfog() : CameraFogBuilder– manage client-side fog (atmosphere) layers (see §2.5).controlScheme() : ControlSchemeBuilder– fluent control scheme sender (see §2.7).attachToEntity(Entity|int $entityOrRuntimeId) : self– attach camera to an entity or runtime ID, e.g. POV spectator (see §2.8).detachFromEntity() : self– detach camera from the current entity (see §2.8).hud(HudPreset|string $presetOrName) : self– apply a HUD preset by instance or registry name (see §6).aimAssist() : AimAssistBuilder– configure and send a camera aim‑assist packet (see §7.4).spline() : CameraSplineBuilder(deprecated, do not use in production)shake(float $intensity = 0.5, float $duration = 1.0, int $type = CameraShakePacket::TYPE_POSITIONAL) : selfstopShake(int $type = CameraShakePacket::TYPE_POSITIONAL) : selfclear() : selfsendPacket(ClientboundPacket $pk) : selfstop() : selfaddTimelineTask(TaskHandler $task) : selfemitSignal(string $signalName) : self– resume a timeline waiting for this signal (see §3.1).
$session->set()
->preset("minecraft:free")
->position($pos)
->rotation(30, 90)
->ease(CameraSetInstructionEaseType::LINEAR, 2.0)
->send();- Or, if you prefer to orient the camera toward a specific point:
use pocketmine\math\Vector3;
$target = new Vector3(0, 64, 0); // Look at this world-space position
$session->set()
->preset("minecraft:free")
->position($pos)
->rotationTo($target) // compute pitch/yaw so the camera looks at $target
->send();- Key methods
preset(string $preset) : self- e.g.
"minecraft:first_person","minecraft:third_person","minecraft:free", or your custom presets.
- e.g.
ease(int $type, float $duration) : self- Use
CameraSetInstructionEaseTypeconstants (e.g.EaseType::LINEAR).
- Use
position(Vector3 $position) : selfpositionOffset(Vector3 $offset) : self– world-space offset from the player's current position.positionLocal(Vector3 $offset) : self– local-space offset relative to the player's view (X: right/left, Y: up/down, Z: forward/backward).rotation(float $pitch, float $yaw) : selfrotationTo(Vector3 $target) : self– compute and set rotation so the camera at the current position looks at the given world-space target.facing(Vector3 $position) : selffacingOffset(Vector3 $offset) : self– world-space offset for the target the camera should look at.facingLocal(Vector3 $offset) : self– local-space offset for the facing target, using the same convention aspositionLocal().viewOffset(Vector2 $offset) : selfentityOffset(Vector3 $offset) : selfsetDefault(bool $value = true) : selfsend() : CameraSession
World vs local offsets
- World-space helpers (
positionOffset,facingOffset) simply add the given offset to the player's current world position. - Local-space helpers (
positionLocal,facingLocal) interpret the offset in the player's view space:- X: right (+) / left (-)
- Y: up (+) / down (-)
- Z: forward (+) / backward (-)
use pocketmine\color\Color;
$session->fade()
->in(0.5) // Fade in over 0.5 seconds
->stay(1.0) // Stay fully opaque for 1 second
->out(0.5) // Fade out over 0.5 seconds
->color(Color::fromRGB(255, 0, 0)) // Red curtain color
->send();- Methods
in(float $seconds) : selfstay(float $seconds) : selfout(float $seconds) : selfcolor(Color $color) : selfsend() : CameraSession
use pocketmine\math\Vector3;
$session->target()
->entity($targetEntity)
->offset(new Vector3(0, 1.6, 0)) // Look at the entity's head
->send();- Methods
offset(Vector3 $offset) : selfentity(?Entity $entity) : self– set or clear the tracked entity (null to clear).entityId(?int $id) : self– set or clear the tracked entity ID (null to clear).send() : CameraSession
use pocketmine\network\mcpe\protocol\types\camera\CameraSetInstructionEaseType as EaseType;
$session->fov()
->set(90.0)
->ease(EaseType::IN_CUBIC, 1.0)
->send();- Methods
set(float $fov) : self— default is 70.ease(int $type, float $duration) : selfsend() : CameraSession
Controls client-side fog layers (e.g. vanilla biome fogs like Nether or Crimson Forest). Fog is managed as a stack similar to the vanilla /fog command: each layer has a fogId and a userProvidedId. You can push the same fog ID multiple times with different userProvidedIds; remove($userProvidedId) removes all layers that were pushed with that id; send() transmits the current stack as a single packet.
Vanilla fog IDs are available as constants in kim\present\cameraapi\utils\VanillaFogIds (e.g. VanillaFogIds::FOG_HELL, VanillaFogIds::FOG_CRIMSON_FOREST, VanillaFogIds::FOG_THE_END). See that class for the full list.
use kim\present\cameraapi\utils\VanillaFogIds;
// Add fog (e.g. Nether-style atmosphere) with a userProvidedId
$session->fog()
->push(VanillaFogIds::FOG_HELL, "nether_phase")
->send();
// Remove a fog layer
$session->fog()
->remove("nether_phase")
->send();
// Multiple layers (stack order preserved)
$session->fog()
->push(VanillaFogIds::FOG_CRIMSON_FOREST, "layer1")
->push(VanillaFogIds::FOG_HELL, "layer2")
->send();- Methods
push(string $fogId, string $userProvidedId) : self– push a fog layer; the same fog ID can be pushed multiple times with different user IDs.remove(string $userProvidedId) : self– remove all layers that were pushed with the givenuserProvidedId.send() : CameraSession– send the current fog stack to the client.
// Apply camera shake
$session->shake(0.8, 2.0);
// Stop shaking
$session->stopShake();
// Clear all camera instructions and reset
$session->clear();Sends a control scheme packet to the player to change how movement/input is interpreted (e.g. camera-relative vs player-relative). Use the predefined packets from ControlSchemePackets. Some schemes require a specific camera preset (e.g. follow_orbit or fixed_boom) to take effect.
use kim\present\cameraapi\Camera;
$session = Camera::of($player);
// Lock player-relative strafe (commonly used with free camera)
$session->controlScheme()->lockedPlayerRelativeStrafe();
// Camera-relative controls (requires follow_orbit or fixed_boom preset)
$session->set()->preset("minecraft:follow_orbit")->send();
$session->controlScheme()->cameraRelative();
// Name-based sending (case-insensitive)
$session->controlScheme()->byName("CAMERA_RELATIVE_STRAFE");- Available schemes (from
ControlSchemePackets)LOCKED_PLAYER_RELATIVE_STRAFE– default / free camera style.CAMERA_RELATIVE– movement relative to camera (requiresfollow_orbitorfixed_boom).CAMERA_RELATIVE_STRAFE– same as above with strafe (requiresfollow_orbitorfixed_boom).PLAYER_RELATIVE– player-relative (requiresfixed_boom).PLAYER_RELATIVE_STRAFE– player-relative with strafe (requiresfixed_boom).
Attaches the player's camera to an entity so they see through that entity's eyes (POV spectator). Accepts either an
Entity (uses its runtime ID via getId()) or an int runtime ID directly. The entity must exist and be visible to
the client when the packet is sent. For the attachment to have a visible effect, the active camera preset must support
entity attachment, such as "minecraft:follow_orbit" or "minecraft:fixed_boom".
use kim\present\cameraapi\Camera;
use pocketmine\entity\Entity;
$session = Camera::of($player);
// Attach camera to another player or entity (see through their eyes)
$session->attachToEntity($targetEntity);
// Or pass the entity runtime ID directly
$session->attachToEntity($targetEntity->getId());
// Later: detach and return to normal view
$session->detachFromEntity();- attachToEntity(Entity|int $entityOrRuntimeId) : self – sends a camera instruction to attach to the given entity (uses
getId()) or runtime ID. - detachFromEntity() : self – sends a camera instruction to detach from the current entity.
CameraTimeline lets you queue multiple camera instructions in time order and play them as a cutscene.
use kim\present\cameraapi\timeline\CameraTimeline;
$timeline = new CameraTimeline();
$timeline
->fade(fn($b) => $b->in(0.5)->stay(0.5)->out(0.5))
->wait(0.5)
->set(fn($b) => $b->preset("minecraft:free")->position($pos))
->wait(3.0)
->shake(0.6, 1.5)
->wait(1.0)
->clear();
$timeline->play($player);- Method overview
wait(float $seconds) : selfwaitUntil(string $signalName) : self– pause the timeline until the given signal is emitted for that player'sCameraSession(see below).set(\Closure(CameraSetBuilder): mixed $setup) : selffade(\Closure(CameraFadeBuilder): mixed $setup) : selftarget(\Closure(CameraTargetBuilder): mixed $setup) : selffov(\Closure(CameraFovBuilder): mixed $setup) : selffog(\Closure(CameraFogBuilder): mixed $setup) : self– add a fog instruction (push/remove layers) at this point in the timeline.controlScheme(ClientboundControlSchemeSetPacket $packet) : self– send a control scheme packet at this point (useControlSchemePackets::…()).attachToEntity(Entity|int $entityOrRuntimeId) : self– attach camera to the entity or runtime ID at this point in the timeline.detachFromEntity() : self– detach camera from the entity at this point.spline(\Closure(CameraSplineBuilder): mixed $setup) : selfshake(float $intensity = 0.5, float $duration = 1.0, int $type = CameraShakePacket::TYPE_POSITIONAL) : selfstopShake(int $type = CameraShakePacket::TYPE_POSITIONAL) : selfclear() : selfsetLoop(bool $loop = true) : self– when enabled, the full sequence automatically restarts after it finishes, until the underlyingCameraSessionis stopped.play(Player $player) : void
To loop a timeline indefinitely until you clear/stop the camera session:
$timeline
->set(/* ... */)
->wait(2.0)
->clear()
->setLoop() // enable looping
->play($player);Note: spline() uses CameraSplineBuilder under the hood, which is currently marked deprecated due to client
crash / disconnect issues.
waitUntil() lets you pause a timeline until an external event (boss spawn, door open, etc.) explicitly resumes it.
This avoids per-tick polling and integrates cleanly with PocketMine-MP's single-threaded scheduler.
Concept
- In the middle of a timeline, insert
->waitUntil("some_signal"). - The timeline stops scheduling further actions when it reaches that point.
- Later, another plugin or event handler calls
$session->emitSignal("some_signal")for each player that should resume.
Example – boss spawn cutscene
use kim\present\cameraapi\Camera;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
function playBossIntro(Player $player, Vector3 $doorPos, Vector3 $bossPos) : void{
$timeline = Camera::timeline()
->set(fn($b) => $b->preset("minecraft:free")->position($doorPos))
->wait(2.0)
->waitUntil("boss_spawned") // pause here until the boss actually spawns
->set(fn($b) => $b->position($bossPos))
->shake(0.8, 2.0)
->wait(2.0)
->clear();
$timeline->play($player);
}In your boss plugin or event listener:
use kim\present\cameraapi\Camera;
public function onBossSpawn(BossSpawnEvent $event) : void{
foreach($event->getPlayersInArena() as $player){
$session = Camera::of($player);
$session->emitSignal("boss_spawned");
}
}Notes
waitUntil()executes the timeline in chunks (segments between signals) and stops scheduling the next chunk until the specified signal is emitted.- This allows you to implement complex cutscene state machines without any per-tick polling.
You can also describe timelines in JSON (or arrays) and load them at runtime.
This is useful if non-programmers (builders / designers) need to tweak cutscenes without touching PHP code.
Supported JSON step types
-
wait–{ "type": "wait", "seconds": 2.0 } -
waitUntil–{ "type": "waitUntil", "signal": "boss_spawned" } -
shake–{ "type": "shake", "intensity": 0.8, "duration": 1.5 } -
stopShake–{ "type": "stopShake" } -
clear–{ "type": "clear" } -
set– camera position / preset:{ "type": "set", "preset": "minecraft:free", "position": [100, 60, 100], "rotation": [30, 90], "facing": [100, 60, 120], "ease": { "type": 0, "duration": 1.0 } } -
fade– screen fade:{ "type": "fade", "in": 0.5, "stay": 1.0, "out": 0.5 } -
fov– field of view:{ "type": "fov", "set": 90.0, "ease": { "type": 0, "duration": 1.0 } } -
fog– push and/or remove fog layers.push: array of{ "fogId": "...", "userProvidedId": "..." }.remove: array ofuserProvidedIdstrings. Order: remove then push.{ "type": "fog", "push": [ { "fogId": "minecraft:fog_hell", "userProvidedId": "phase1" } ], "remove": ["phase0"] } -
controlScheme– send a control scheme packet.schememust be one of:LOCKED_PLAYER_RELATIVE_STRAFE,CAMERA_RELATIVE,CAMERA_RELATIVE_STRAFE,PLAYER_RELATIVE,PLAYER_RELATIVE_STRAFE:{ "type": "controlScheme", "scheme": "LOCKED_PLAYER_RELATIVE_STRAFE" } -
target– set or clear camera target entity. UseentityId(runtime ID). Optionaloffset[x, y, z]:{ "type": "target", "entityId": 42, "offset": [0, 1.6, 0] }Omit
entityId(or use null) to clear target. -
attachToEntity– attach camera to an entity by runtime ID (POV spectator). UseentityIdorruntimeId:{ "type": "attachToEntity", "entityId": 42 } -
detachFromEntity– detach camera from the current entity:{ "type": "detachFromEntity" }
Full example – boss_intro.json
{
"loop": false,
"steps": [
{
"type": "fade",
"in": 0.5,
"stay": 1.0,
"out": 0.5
},
{
"type": "set",
"preset": "minecraft:free",
"position": [100, 60, 100],
"rotation": [30, 90]
},
{
"type": "fov",
"set": 90.0,
"ease": { "type": 0, "duration": 1.0 }
},
{
"type": "wait",
"seconds": 2.0
},
{
"type": "shake",
"intensity": 0.8,
"duration": 1.5
},
{
"type": "wait",
"seconds": 1.5
},
{
"type": "clear"
}
]
}Loading a JSON timeline
use kim\present\cameraapi\Camera;
$json = file_get_contents($this->getDataFolder() . "cutscenes/boss_intro.json");
$timeline = Camera::loadTimeline($json);
$timeline->play($player);On the client, preset names like "minecraft:free" are resolved from a list sent by the server.
This plugin registers vanilla presets and provides an API for registering your own.
- Built-in constants
PRESET_FIRST_PERSON = "minecraft:first_person"PRESET_FIXED_BOOM = "minecraft:fixed_boom"PRESET_FOLLOW_ORBIT = "minecraft:follow_orbit"PRESET_FREE = "minecraft:free"PRESET_THIRD_PERSON = "minecraft:third_person"PRESET_THIRD_PERSON_FRONT = "minecraft:third_person_front"
use kim\present\cameraapi\camera\preset\CameraPresetBuilder;
use kim\present\cameraapi\camera\preset\CameraPresetRegistry;
use pocketmine\math\Vector3;
CameraPresetRegistry::register(
CameraPresetBuilder::create("myplugin:topdown")
->setPos(new Vector3(0, 50, 0))
->setPitch(-90.0)
->build()
);- You can call this after the server starts (e.g. in another plugin's
onEnable()),
and then re-sync presets to a specific player viaCameraPresetRegistry::sendTo($player).
use kim\present\cameraapi\camera\preset\CameraPresetRegistry;
$id = CameraPresetRegistry::getIdByName("myplugin:topdown"); // int|nullCameraSetBuilder::preset() internally uses getIdByName(), so in most cases you only need to pass the preset name
string.
Camera markers are small in-world entities you can spawn to define camera positions and orientations. They appear as fake players (with a plugin-supplied skin), support yaw/pitch from the spawn location, and can have an interact button and callbacks for left-click (attack) and right-click (interact). Use them for map creation or cutscene keyframes.
- Spawning:
Camera::spawnMarker(Location $location, ?string $label = null) : CameraMarker$location– world position and rotation (yaw/pitch) for the marker; e.g. use the player's location (optionally offset to eye height) so the marker faces the same direction.
- Methods on
CameraMarkersetPosition(Vector3 $pos),lookAt(Vector3 $target),setNameTag(string $name),setInteractButton(string $buttonText)setOnAttack(?\Closure $onAttack)– callback when a player left-clicks the marker (e.g. remove it).setOnClick(?\Closure $onClick)– callback when a player right-clicks the marker (e.g. apply marker to camera and play a timeline).applyToSession(CameraSession $session, ?int $easeType = null, ?float $easeDuration = null)– sets the camera to the marker's position and rotation (vanilla FREE preset). Optionally pass an ease type (e.g.CameraSetInstructionEaseType::LINEAR) and duration in seconds for a smooth transition.remove()– despawns the marker.
Example: create a marker, remove on attack, preview on interact
use kim\present\cameraapi\Camera;
use kim\present\cameraapi\marker\CameraMarker;
use kim\present\cameraapi\session\CameraSession;
use pocketmine\player\Player;
use pocketmine\utils\TextFormat;
// Create a marker at the player's eye position, facing the same direction
$markerLocation = $sender->getLocation();
$markerLocation->y += $sender->getEyeHeight();
$this->markers[$markerName] = Camera::spawnMarker($markerLocation, $markerName)
->setOnAttack(function($_, Player $player) use ($markerName) : void{
$this->markers[$markerName]->remove();
unset($this->markers[$markerName]);
$player->sendMessage(TextFormat::GREEN . "Removed '$markerName' camera marker.");
})
->setInteractButton("Test")
->setOnClick(fn(CameraMarker $marker, Player $player) => Camera::timeline()
->add(fn(CameraSession $session) => $marker->applyToSession($session))
->wait(3)
->clear()
->play($player)
);
$sender->sendMessage(TextFormat::GREEN . "Created '$markerName' camera marker.");- Right-clicking the marker applies its pose to the player's camera, holds for 3 seconds, then clears. Left-clicking removes the marker.
Using ease for smooth transitions
You can pass an ease type and duration so the camera moves smoothly to the marker's pose instead of snapping:
use pocketmine\network\mcpe\protocol\types\camera\CameraSetInstructionEaseType as EaseType;
// Instant (default)
$marker->applyToSession($session);
// 1.5 second linear transition to the marker's position and rotation
$marker->applyToSession($session, EaseType::LINEAR, 1.5);
// 2 second cubic ease-in transition
$marker->applyToSession($session, EaseType::IN_CUBIC, 2.0);CameraAPI provides HUD presets: immutable layouts that describe which HudElement entries should be visible.
They can be applied directly to a Player or CameraSession via send(), or via CameraSession::hud() (see below). Optional presets can be stored in HudPresetRegistry.
- Classes
kim\present\cameraapi\hud\HudPreset– immutable preset value objectkim\present\cameraapi\hud\HudPresetBuilder– fluent builder for presetskim\present\cameraapi\hud\HudPresetRegistry– string-keyed registry of presets
HudPreset
readonlyclass with oneboolproperty per HUD element:paperDoll,armor,tooltips,touchControls,crosshair,hotbar,health,xp,food,airBubbles,horseHealth,statusEffects,itemText
- Constructor defaults every flag to
trueso you can use named parameters to turn off only what you need:
use kim\present\cameraapi\hud\HudPreset;
// Hide everything (e.g. for a clean cinematic shot)
$clear = new HudPreset(
paperDoll: false,
armor: false,
tooltips: false,
touchControls: false,
crosshair: false,
hotbar: false,
health: false,
xp: false,
food: false,
airBubbles: false,
horseHealth: false,
statusEffects: false,
itemText: false,
);
$clear->send($player);
// or
$clear->send(Camera::of($player));-
send(CameraSession|Player $target) : void
Applies this preset to the target: hides all known HUD elements, then re-enables those flaggedtruein this preset. No-op if the player is offline or disconnected. -
getVisibleElements() : list<HudElement>
Returns the list ofHudElementenum cases that are enabled in this preset. -
fromVisibleElements(list<HudElement> $visibleElements) : self
Static factory: builds a preset where only the given HUD elements are visible; all others are hidden.
HudPresetBuilder
Fluent builder if you prefer a chainable API over named parameters:
create() : self– new builder with all elements enabled.- Per-element setters –
paperDoll(bool $value = true),armor(bool $value = true), …,itemText(bool $value = true). hideAll() : self– sets every element to false.build() : HudPreset– returns an immutable preset from the current state.
use kim\present\cameraapi\hud\HudPresetBuilder;
$minimal = HudPresetBuilder::create()
->hideAll()
->hotbar(true)
->health(true)
->build();
$minimal->send($player);HudPresetRegistry
-
Built-in presets
HudPresetRegistry::PRESET_DEFAULT– all HUD elements visible (new HudPreset()).HudPresetRegistry::PRESET_CLEAR– all HUD elements hidden (HudPreset::fromVisibleElements([])).
-
API
register(string $name, HudPreset $preset) : void– register or overwrite a preset by name (case-insensitive).get(string $name) : ?HudPreset– get a preset by name.isRegistered(string $name) : bool– whether a preset with that name exists.getAll() : array<string, HudPreset>– all registered presets (built-in + custom), keyed by lowercased name.
Applying HUD from a session
You can apply a preset from a CameraSession with hud(HudPreset|string): pass either a HudPreset instance or a name registered in HudPresetRegistry. Returns the session for chaining.
use kim\present\cameraapi\Camera;
use kim\present\cameraapi\hud\HudPresetRegistry;
$session = Camera::of($player);
// By registry name (e.g. hide all HUD for a cutscene)
$session->hud(HudPresetRegistry::PRESET_CLEAR);
// By preset instance
$session->hud(new HudPreset(crosshair: true, hotbar: true));Example – registry and custom preset
use kim\present\cameraapi\hud\HudPresetRegistry;
// Apply built-in “all hidden” preset
$preset = HudPresetRegistry::get(HudPresetRegistry::PRESET_CLEAR);
if($preset !== null){
$preset->send($player);
}use kim\present\cameraapi\hud\HudPresetBuilder;
use kim\present\cameraapi\hud\HudPresetRegistry;
// Register a custom preset and use it later
$minimal = HudPresetBuilder::create()->hideAll()->hotbar(true)->build();
HudPresetRegistry::register("minimal_hotbar", $minimal);
$preset = HudPresetRegistry::get("minimal_hotbar");
if($preset !== null){
$preset->send($player);
}CameraAPI also manages camera aim‑assist presets and categories and synchronizes them to the client via
CameraAimAssistPresetsPacket. This wraps the vanilla "minecraft:aim_assist_default" preset and lets you extend it
with your own categories / presets if you need per‑item aim‑assist behavior.
-
Classes
kim\present\cameraapi\aimassist\AimAssistPresetRegistry– static registry for aim‑assist categories and presets.kim\present\cameraapi\aimassist\VanillaAimAssistPresetIds– constants for vanilla IDs:MINECRAFT_DEFAULT = 'minecraft:aim_assist_default'CATEGORY_BUCKET = 'minecraft:bucket'CATEGORY_EMPTY_HAND = 'minecraft:empty_hand'CATEGORY_DEFAULT = 'minecraft:default'
- PMMP DTOs (from
pocketmine\network\mcpe\protocol\types\camera):CameraAimAssistCategory,CameraAimAssistCategoryPriorities,CameraAimAssistCategoryPriorityCameraAimAssistPreset,CameraAimAssistPresetExclusionDefinition,CameraAimAssistPresetItemSettings
-
What the plugin does automatically
- On plugin enable,
AimAssistPresetRegistry::init()is called fromMain::onEnable(). AimAssistPresetRegistryregisters the vanilla presetminecraft:aim_assist_defaultand three categories:minecraft:bucket– higher priority for cauldrons and liquids when holding a bucket.minecraft:empty_hand– higher priority for various log blocks when empty‑handed.minecraft:default– higher priority for buttons / levers.
- After initialization and on every registry change,
AimAssistPresetRegistry::sendToAll()broadcasts the full category + preset list to all online players. - On
PlayerJoinEvent,MaincallsAimAssistPresetRegistry::sendTo($player)so the joining client receives the current list.
- On plugin enable,
If you only want vanilla‑style behavior, you usually do not need to call anything: the plugin already registers and
sends the minecraft:aim_assist_default preset and categories.
To explicitly re‑send the list or a single preset to a player:
use kim\present\cameraapi\aimassist\AimAssistPresetRegistry;
use kim\present\cameraapi\aimassist\VanillaAimAssistPresetIds;
use pocketmine\player\Player;
function syncAimAssist(Player $player) : void{
// Send full category + preset list (already done on join, but safe to call again)
AimAssistPresetRegistry::sendTo($player);
// Or send only the vanilla default preset (categories included)
AimAssistPresetRegistry::sendPresetToPlayer(
$player,
VanillaAimAssistPresetIds::MINECRAFT_DEFAULT
);
}Note: Client‑side toggling of aim assist is controlled by
ClientCameraAimAssistPacket.
CameraAPI currently focuses on defining and syncing the preset list, not listening to or sending client‑toggle packets for you.
The current API is intentionally low‑level: you build PMMP DTOs directly and pass them into the registry.
use kim\present\cameraapi\aimassist\AimAssistPresetRegistry;
use pocketmine\network\mcpe\protocol\types\camera\CameraAimAssistCategory;
use pocketmine\network\mcpe\protocol\types\camera\CameraAimAssistCategoryPriorities;
use pocketmine\network\mcpe\protocol\types\camera\CameraAimAssistCategoryPriority;
use pocketmine\network\mcpe\protocol\types\camera\CameraAimAssistPreset;
use pocketmine\network\mcpe\protocol\types\camera\CameraAimAssistPresetExclusionDefinition;
use pocketmine\network\mcpe\protocol\types\camera\CameraAimAssistPresetItemSettings;
function registerBowAimAssistPreset() : void{
// 1) Category: prefer "target dummy" blocks when holding a bow
$priorities = new CameraAimAssistCategoryPriorities(
entities: [],
blocks: [
new CameraAimAssistCategoryPriority('mygame:target_dummy', 60),
],
blockTags: [],
entityTypeFamilies: [],
defaultEntityPriority: null,
defaultBlockPriority: 30,
);
$categoryName = 'mygame:bow';
AimAssistPresetRegistry::registerCategory(
new CameraAimAssistCategory($categoryName, $priorities)
);
// 2) Preset: map bow item to the category
$exclusion = new CameraAimAssistPresetExclusionDefinition(
blocks: [],
entities: [],
blockTags: [],
entityTypeFamilies: [],
);
$presetId = 'mygame:aim_assist_bow';
$preset = new CameraAimAssistPreset(
$presetId,
$exclusion,
liquidTargetingList: [],
itemSettings: [
new CameraAimAssistPresetItemSettings('minecraft:bow', $categoryName),
],
defaultItemSettings: $categoryName,
defaultHandSettings: null,
);
AimAssistPresetRegistry::registerPreset($preset);
}- When you call
registerCategory()/registerPreset(), the registry automatically callssendToAll()so all clients see the updated aim‑assist configuration.
Right now, the aim‑assist API is somewhat low‑level and verbose because it directly exposes PMMP DTOs. To make it easier to use in real plugins, the following improvements are planned:
- Builder API for aim‑assist (similar to camera presets / HUD):
AimAssistCategoryBuilder– fluent helpers for adding block/entity priorities and defaults.AimAssistPresetBuilder– helpers for exclusions, liquid targeting, and item mappings.
- Bulk registration without auto‑broadcast:
- Option to disable the current
sendToAll()side‑effect inregisterCategory()/registerPreset(), or abeginBatch()/endBatch()API that only sends once at the end.
- Option to disable the current
- Config‑driven definitions:
- Load categories/presets from YAML/JSON files so designers can tweak behavior without PHP changes.
- Higher‑level helpers for common cases:
- e.g.
registerSimpleBlockPriorityCategory(string $name, list<string> $blocks, int $priority = 60)to cut down on boilerplate when you only care about blocks.
- e.g.
Until these improvements land, the recommended way to use aim assist is:
- Rely on the plugin’s built‑in
minecraft:aim_assist_defaultwhere possible. - For custom behavior, create small helper functions in your own plugin (as in §7.2) that hide the DTO boilerplate.
For most gameplay code, you should not have to touch CameraAimAssistPresetsPacket or CameraAimAssistPacket
directly. Instead, use the CameraSession::aimAssist() builder so you can start from Camera::of($player):
use kim\present\cameraapi\Camera;
use kim\present\cameraapi\aimassist\VanillaAimAssistPresetIds;
// Enable the built-in entity-only aim-assist preset with default angle/distance
$session = Camera::of($player);
$session->aimAssist()
->preset(VanillaAimAssistPresetIds::ENTITY_ONLY)
->send();If you need to override angle, distance, or target mode:
use kim\present\cameraapi\Camera;
use kim\present\cameraapi\aimassist\VanillaAimAssistPresetIds;
use pocketmine\network\mcpe\protocol\types\camera\CameraAimAssistTargetMode;
Camera::of($player)->aimAssist()
->preset(VanillaAimAssistPresetIds::MINECRAFT_DEFAULT)
->viewAngle(60.0, 45.0) // yaw/pitch half-angles
->distance(24.0) // max targeting distance
->targetMode(CameraAimAssistTargetMode::ANGLE) // or ::DISTANCE
->send();To clear an active aim-assist configuration, use CameraAimAssistActionType::CLEAR:
use kim\present\cameraapi\Camera;
use pocketmine\network\mcpe\protocol\types\camera\CameraAimAssistActionType;
Camera::of($player)->aimAssist()
->action(CameraAimAssistActionType::CLEAR)
->send();-
Plugin entry (
Main)- Extends PMMP
PluginBaseand implementsListener. - Calls
CameraSessionManager::init()and registers event listeners inonEnable(). - Initializes and syncs both camera presets and aim‑assist presets:
- On
PlayerJoinEvent: creates session + callsCameraPresetRegistry::sendTo($player)andAimAssistPresetRegistry::sendTo($player). - On
PlayerQuitEvent: removes session.
- On
- Hooks
DataPacketSendEventand sets theexperimental_creator_camerasflag onStartGamePacket/ResourcePackStackPacketExperiments.
- Extends PMMP
-
Session management (
CameraSessionManager)- Uses an internal
\WeakMap<Player, CameraSession>so sessions are automatically cleaned up with the player objects. - Provides
createSession(),getSession(),removeSession().
- Uses an internal
-
Timeline (
CameraTimeline)- Uses the server scheduler and
ClosureTaskto convert seconds to ticks (20 ticks/sec) and schedule actions. - Stores
TaskHandlerinstances in theCameraSessionand cancels them onsession->stop()to avoid memory leaks and orphaned tasks.
- Uses the server scheduler and
-
Dependencies
- PocketMine-MP 5.x network packets and camera types.
- PHP 8
WeakReference/WeakMap.
use kim\present\cameraapi\Camera;
use pocketmine\event\Listener;
use pocketmine\event\player\PlayerJoinEvent;
use pocketmine\math\Vector3;
class IntroListener implements Listener{
public function onJoin(PlayerJoinEvent $event) : void{
$player = $event->getPlayer();
$session = Camera::of($player);
$spawn = $player->getWorld()->getSpawnLocation()->add(0, 10, 0);
$session->fade()
->in(0.5)->stay(0.5)->out(0.5)
->send();
$session->set()
->preset("minecraft:free")
->position($spawn)
->facing($player->getPosition())
->send();
}
}use kim\present\cameraapi\timeline\CameraTimeline;
use kim\present\cameraapi\Camera;
use kim\present\cameraapi\utils\VanillaFogIds;
use pocketmine\math\Vector3;
function playBossCutscene(Player $player, Vector3 $bossPos) : void{
$timeline = new CameraTimeline();
$timeline
->fade(fn($b) => $b->in(0.5)->stay(0.5))
->wait(0.5)
->set(fn($b) => $b
->preset("minecraft:free")
->position($bossPos->add(0, 15, -10))
->facing($bossPos)
)
->fog(fn($b) => $b->push(VanillaFogIds::FOG_HELL)) // Add Nether-style fog for the boss phase
->wait(3.0)
->shake(0.7, 2.0)
->wait(1.0)
->fog(fn($b) => $b->remove(VanillaFogIds::FOG_HELL)) // Clear fog before reset
->clear();
$timeline->play($player);
}use kim\present\cameraapi\camera\preset\CameraPresetBuilder;
use kim\present\cameraapi\camera\preset\CameraPresetRegistry;
use kim\present\cameraapi\Camera;
use pocketmine\math\Vector3;
function registerTopdownPreset() : void{
CameraPresetRegistry::register(
CameraPresetBuilder::create("mygame:topdown")
->setPos(new Vector3(0, 30, 0))
->setPitch(-90.0)
->build()
);
}
function applyTopdownCamera(Player $player) : void{
CameraPresetRegistry::sendTo($player);
Camera::of($player)->set()
->preset("mygame:topdown")
->send();
}- Reuse sessions
Camera::of($player)internally caches sessions, so calling it repeatedly is cheap and recommended.
- Timeline overlapping
CameraTimeline::play()callsCameraSession::stop()first, cancelling any existing timeline tasks for that player.- If you want multiple overlapping timelines, you must manage scheduling and cancellation yourself.
- Avoid
spline()for nowCameraSession::spline()andCameraTimeline::spline()useCameraSplineBuilder, which is currently known to cause client crashes / disconnects and is marked deprecated.
- Experimental flag conflicts
- This plugin automatically enables
experimental_creator_cameras, but if other plugins also modifyStartGamePacketorResourcePackStackPacket, be careful about interaction and ordering.
- This plugin automatically enables
- Exceptions & edge cases
- Most APIs are designed to safely no-op if the player is offline or the packet cannot be sent.
- If you reference a non-registered preset name,
CameraPresetRegistry::get()may throw\InvalidArgumentException.- Prefer registering all presets during your plugin's initialization and avoid typos in preset names.