- 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.42.0or higher (plugin.ymlapi). The spline and rotation-easing packets need BedrockProtocol 56 (Bedrock 1.26.10).
- 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).aimAssistActorPriority() : AimAssistActorPriorityBuilder– override the aim‑assist priority of single actors (see §7.5).isClientAimAssistAllowed() : ?bool– aim assist setting last reported by the client (null until it reports; see §7.5).reapply() : self– send the remembered camera / FOV / target / attachment / HUD / control scheme / fog state again (see §2.9).isTimelinePlaying() : bool/getWaitingSignal() : ?string– query the running timeline (see §3).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– required;send()throws if it is missing or not registered.- 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, bool $followPitch = false) : 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, bool $followPitch = false) : self– local-space offset for the facing target, using the same convention aspositionLocal().viewOffset(Vector2 $offset) : selfentityOffset(Vector3 $offset) : selfsetDefault(bool $value = true) : selfremoveIgnoreStartingValues(bool $remove = true) : self– sets the protocol'sremoveIgnoreStartingValuesComponentflag.send() : 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 (-)
- The offset is relative to the player's feet position. By default only the player's yaw is used (forward stays horizontal); pass
$followPitch = trueto tilt the forward/up axes by the player's pitch as well.
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– sends a target instruction, or a "remove target" instruction when no entity is set (e.g.$session->target()->send()removes the current target).
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) : selfclear(bool $clear = true) : self— clear the FOV set by previous instructions (like/camera fov_clear).send() : 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. The stack is kept per player on the CameraSession, so every fog() call starts from the stack that was last sent.
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 = null) : self– push a fog layer; the same fog ID can be pushed multiple times with different user IDs.userProvidedIddefaults tofogId.remove(string $userProvidedId) : self– remove all layers that were pushed with the givenuserProvidedId.clear() : self– remove every layer.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.
The session remembers what it sent to the client: the active camera set instruction, FOV, target, the attached
entity, the last HUD preset (applied through the plugin), the control scheme and the fog stack. They are cleared again
by clear(), a FOV clear, removing the target or detaching.
- Getters:
getCurrentSet(),getCurrentFov(),getCurrentTarget(),getAttachedEntityId(),getCurrentHud(),getCurrentControlScheme(). reapply()sends that state again, e.g. after the client may have lost it (respawn, dimension change). Eases are dropped so nothing animates twice. Nothing is reapplied automatically; call it from your own event handler.
public function onRespawn(PlayerRespawnEvent $event) : void{
Camera::of($event->getPlayer())->reapply();
}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->preset("minecraft:free")->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, "shakeType": "positional" }(shakeType:"positional"(default) or"rotational") -
stopShake–{ "type": "stopShake", "shakeType": "positional" } -
clear–{ "type": "clear" } -
set– camera position / preset (presetis required; loading fails without it):{ "type": "set", "preset": "minecraft:free", "position": [100, 60, 100], "rotation": [30, 90], "facing": [100, 60, 120], "ease": { "type": "in_out_sine", "duration": 1.0 } }ease.typeaccepts either aCameraSetInstructionEaseTypevalue (e.g.0) or the vanilla/cameraease name (e.g."linear","in_out_sine","out_bounce"). The same applies tofov. -
fade– screen fade:{ "type": "fade", "in": 0.5, "stay": 1.0, "out": 0.5, "color": [255, 0, 0] } -
fov– field of view:{ "type": "fov", "set": 90.0, "ease": { "type": 0, "duration": 1.0 } }Use
"clear": trueto clear the FOV instead (like/camera fov_clear). -
fog– push and/or remove fog layers.push: array of{ "fogId": "...", "userProvidedId": "..." }(userProvidedIddefaults tofogId).remove: array ofuserProvidedIdstrings. Order: remove then push. Layers persist between steps (the stack is stored per player).{ "type": "fog", "push": [ { "fogId": "minecraft:fog_hell", "userProvidedId": "phase1" } ], "remove": ["phase0"] } -
controlScheme– send a control scheme packet.schememust be one of (case-insensitive):LOCKED_PLAYER_RELATIVE_STRAFE,CAMERA_RELATIVE,CAMERA_RELATIVE_STRAFE,PLAYER_RELATIVE,PLAYER_RELATIVE_STRAFE. Unknown names fail when loading:{ "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" } -
hud– apply a HUD preset registered inHudPresetRegistry(e.g.DEFAULT,CLEAR); unknown names fail when loading:{ "type": "hud", "preset": "CLEAR" } -
aimAssist– activate an aim‑assist preset, or clear it whenpresetis omitted /"clear": true. OptionalviewAngle[horizontal, vertical],distance(1–16),targetMode("angle"/"distance") anddebug:{ "type": "aimAssist", "preset": "cameraapi:aim_assist_entity_only", "distance": 12 }
set steps also accept viewOffset [x, y], entityOffset [x, y, z] and default (bool). Steps with an unknown type are skipped, and a warning is written to the server log.
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|nullCameraPresetBuilder::setAudioListenerType() is unset (null) by default, so a preset inherits the audio listener type
of its parent preset. Set CameraPreset::AUDIO_LISTENER_TYPE_PLAYER / AUDIO_LISTENER_TYPE_CAMERA explicitly if you
want a fixed value.
CameraSetBuilder::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, ?array $viewers = 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.$viewers– optional list ofPlayers that may see the marker (e.g. only admins editing a cutscene);nullshows it to everyone.
- Methods on
CameraMarkersetPosition(Vector3 $pos),lookAt(Vector3 $target),setNameTag(string $name),setInteractButton(string $buttonText)setViewers(?array $players)– change who can see the marker later (null= everyone).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\DefaultAimAssistPresetIds;
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\DefaultAimAssistPresetIds;
// Enable the built-in entity-only aim-assist preset with default angle/distance
$session = Camera::of($player);
$session->aimAssist()
->preset(DefaultAimAssistPresetIds::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(DefaultAimAssistPresetIds::MINECRAFT_DEFAULT)
->viewAngle(60.0, 45.0) // horizontal/vertical angle (> 0)
->distance(12.0) // max targeting distance (1-16)
->targetMode(CameraAimAssistTargetMode::ANGLE) // or ::DISTANCE
->send();To clear an active aim-assist configuration, use clear() (or preset(null)):
use kim\present\cameraapi\Camera;
Camera::of($player)->aimAssist()
->clear()
->send();distance() only accepts 1–16 blocks, viewAngle() needs positive angles, and send() throws
\InvalidArgumentException for a preset that is not registered in AimAssistPresetRegistry.
Camera::of($player)->aimAssistActorPriority()->add($presetId, $categoryId, $actorId, $priority)->send()overrides the priority of an actor listed in a registered category. The builder resolves the preset/category/actor positions that the protocol uses fromAimAssistPresetRegistry(seegetPresetIndex()/getCategoryIndex()). This packet has not been verified against a real client yet.- When a client reports that its aim assist was switched on or off, the plugin stores it
(
CameraSession::isClientAimAssistAllowed()) and callsCameraAimAssistToggleEvent(getPresetId(),getActionType(),isAimAssistAllowed()).
-
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 marked deprecated. On PocketMine-MP 5.40/5.41 (Bedrock 1.26.0) the spline instruction caused client disconnects. It now builds against PocketMine-MP 5.42+, but it has not been verified against a real client yet.
- 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.
CameraPresetRegistry::get()/getIdByName()returnnullfor unknown preset names.CameraSetBuilder::send()throws\LogicExceptionwhen no preset was set and\InvalidArgumentExceptionwhen the preset is not registered.- Presets registered after players joined are re-sent automatically, but registering them during your plugin's initialization is still preferred.