Skip to content
presentkim-pmPublic

About

A high-level camera control API

Resources

Stars

11 stars

Watchers

1 watching

Forks

Latest commit

 

History

86 Commits

Folders and files

Repository files navigation

Version Stars License


Logo

CameraAPI

A high-level camera control API for Minecraft Bedrock (PocketMine-MP 5.x).

Contact to me · Report a bug · Request a feature

Overview

  • What it does
    • Wraps low-level packets like CameraInstructionPacket and CameraPresetsPacket behind a fluent PHP API.
    • Manages per-player CameraSession objects and exposes builder-style helpers for sending camera instructions.
  • 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.

Installation & Requirements

  • Server
    • PocketMine-MP 5.42.0 or higher (plugin.yml api). The spline and rotation-easing packets need BedrockProtocol 56 (Bedrock 1.26.10).
  • Installation
    • Clone this repository or place the CameraAPI plugin folder into your PMMP server's plugins directory.
    • Restart the server; the plugin will load automatically according to plugin.yml.
  • How it works at runtime
    • The Main plugin 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.
    • When StartGamePacket or ResourcePackStackPacket is sent, the plugin automatically enables the experimental_creator_cameras experiment flag.

Quick Start

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();
}

API Documentation

1. Entry Point: Camera

Camera::of(Player $player) : CameraSession
  • Description: Returns the CameraSession for 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) : CameraTimeline
  • timeline() creates an empty, code-driven timeline.
  • loadTimeline() builds a timeline from a JSON description using CameraTimelineParser.

2. CameraSession

The session keeps camera context per player and provides builders and utility methods.

  • Method summary
    • getPlayer() : ?Player
    • set() : CameraSetBuilder
    • fade() : CameraFadeBuilder
    • target() : CameraTargetBuilder
    • fov() : CameraFovBuilder
    • fog() : 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) : self
    • stopShake(int $type = CameraShakePacket::TYPE_POSITIONAL) : self
    • clear() : self
    • sendPacket(ClientboundPacket $pk) : self
    • stop() : self
    • addTimelineTask(TaskHandler $task) : self
    • emitSignal(string $signalName) : self – resume a timeline waiting for this signal (see §3.1).

2.1 Position / Rotation: set() : CameraSetBuilder

$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.
    • ease(int $type, float $duration) : self
      • Use CameraSetInstructionEaseType constants (e.g. EaseType::LINEAR).
    • position(Vector3 $position) : self
    • positionOffset(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) : self
    • rotationTo(Vector3 $target) : self – compute and set rotation so the camera at the current position looks at the given world-space target.
    • facing(Vector3 $position) : self
    • facingOffset(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 as positionLocal().
    • viewOffset(Vector2 $offset) : self
    • entityOffset(Vector3 $offset) : self
    • setDefault(bool $value = true) : self
    • removeIgnoreStartingValues(bool $remove = true) : self – sets the protocol's removeIgnoreStartingValuesComponent flag.
    • 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 = true to tilt the forward/up axes by the player's pitch as well.

2.2 Screen Fade: fade() : CameraFadeBuilder

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) : self
    • stay(float $seconds) : self
    • out(float $seconds) : self
    • color(Color $color) : self
    • send() : CameraSession

2.3 Target Tracking: target() : CameraTargetBuilder

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) : self
    • entity(?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).

2.4 FOV Control: fov() : CameraFovBuilder

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) : self
    • clear(bool $clear = true) : self — clear the FOV set by previous instructions (like /camera fov_clear).
    • send() : CameraSession

2.5 Fog (atmosphere): fog() : CameraFogBuilder

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. userProvidedId defaults to fogId.
    • remove(string $userProvidedId) : self – remove all layers that were pushed with the given userProvidedId.
    • clear() : self – remove every layer.
    • send() : CameraSession – send the current fog stack to the client.

2.6 Shake / Reset

// Apply camera shake
$session->shake(0.8, 2.0);

// Stop shaking
$session->stopShake();

// Clear all camera instructions and reset
$session->clear();

2.7 Control scheme: controlScheme() : ControlSchemeBuilder

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 (requires follow_orbit or fixed_boom).
    • CAMERA_RELATIVE_STRAFE – same as above with strafe (requires follow_orbit or fixed_boom).
    • PLAYER_RELATIVE – player-relative (requires fixed_boom).
    • PLAYER_RELATIVE_STRAFE – player-relative with strafe (requires fixed_boom).

2.8 Attach / Detach entity: attachToEntity(Entity|int) and detachFromEntity()

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.

2.9 State tracking: reapply()

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();
}

3. CameraTimeline (Cutscenes / Sequences)

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) : self
    • waitUntil(string $signalName) : self – pause the timeline until the given signal is emitted for that player's CameraSession (see below).
    • set(\Closure(CameraSetBuilder): mixed $setup) : self
    • fade(\Closure(CameraFadeBuilder): mixed $setup) : self
    • target(\Closure(CameraTargetBuilder): mixed $setup) : self
    • fov(\Closure(CameraFovBuilder): mixed $setup) : self
    • fog(\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 (use ControlSchemePackets::…()).
    • 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) : self
    • shake(float $intensity = 0.5, float $duration = 1.0, int $type = CameraShakePacket::TYPE_POSITIONAL) : self
    • stopShake(int $type = CameraShakePacket::TYPE_POSITIONAL) : self
    • clear() : self
    • setLoop(bool $loop = true) : self – when enabled, the full sequence automatically restarts after it finishes, until the underlying CameraSession is 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.

3.1 Signal-based waiting (no per-tick polling)

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.

3.2 Data-driven timelines (JSON)

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 (preset is 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.type accepts either a CameraSetInstructionEaseType value (e.g. 0) or the vanilla /camera ease name (e.g. "linear", "in_out_sine", "out_bounce"). The same applies to fov.

  • 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": true to clear the FOV instead (like /camera fov_clear).

  • fog – push and/or remove fog layers. push: array of { "fogId": "...", "userProvidedId": "..." } (userProvidedId defaults to fogId). remove: array of userProvidedId strings. 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. scheme must 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. Use entityId (runtime ID). Optional offset [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). Use entityId or runtimeId:

    { "type": "attachToEntity", "entityId": 42 }
  • detachFromEntity – detach camera from the current entity:

    { "type": "detachFromEntity" }
  • hud – apply a HUD preset registered in HudPresetRegistry (e.g. DEFAULT, CLEAR); unknown names fail when loading:

    { "type": "hud", "preset": "CLEAR" }
  • aimAssist – activate an aim‑assist preset, or clear it when preset is omitted / "clear": true. Optional viewAngle [horizontal, vertical], distance (1–16), targetMode ("angle" / "distance") and debug:

    { "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);

4. Presets: CameraPresetRegistry / CameraPresetBuilder

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"

4.1 Registering a preset

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 via CameraPresetRegistry::sendTo($player).

4.2 Looking up / using presets

use kim\present\cameraapi\camera\preset\CameraPresetRegistry;

$id = CameraPresetRegistry::getIdByName("myplugin:topdown"); // int|null

CameraPresetBuilder::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.


5. Camera markers (in-world placement)

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 of Players that may see the marker (e.g. only admins editing a cutscene); null shows it to everyone.
  • Methods on CameraMarker
    • setPosition(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);

6. HUD presets (utility)

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 object
    • kim\present\cameraapi\hud\HudPresetBuilder – fluent builder for presets
    • kim\present\cameraapi\hud\HudPresetRegistry – string-keyed registry of presets

HudPreset

  • readonly class with one bool property per HUD element:
    • paperDoll, armor, tooltips, touchControls, crosshair, hotbar, health, xp, food, airBubbles, horseHealth, statusEffects, itemText
  • Constructor defaults every flag to true so 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 flagged true in this preset. No-op if the player is offline or disconnected.

  • getVisibleElements() : list<HudElement>
    Returns the list of HudElement enum 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);
}

7. Aim‑assist presets (beta)

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, CameraAimAssistCategoryPriority
      • CameraAimAssistPreset, CameraAimAssistPresetExclusionDefinition, CameraAimAssistPresetItemSettings
  • What the plugin does automatically

    • On plugin enable, AimAssistPresetRegistry::init() is called from Main::onEnable().
    • AimAssistPresetRegistry registers the vanilla preset minecraft:aim_assist_default and 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, Main calls AimAssistPresetRegistry::sendTo($player) so the joining client receives the current list.

7.1 Basic usage (vanilla default)

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.

7.2 Registering a custom aim‑assist preset

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 calls sendToAll() so all clients see the updated aim‑assist configuration.

7.3 Current limitations & planned improvements

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 in registerCategory() / registerPreset(), or a beginBatch() / endBatch() API that only sends once at the end.
  • 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.

Until these improvements land, the recommended way to use aim assist is:

  1. Rely on the plugin’s built‑in minecraft:aim_assist_default where possible.
  2. For custom behavior, create small helper functions in your own plugin (as in §7.2) that hide the DTO boilerplate.

7.4 Using aim‑assist from CameraSession

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.

7.5 Actor priorities and the client's aim‑assist toggle

  • 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 from AimAssistPresetRegistry (see getPresetIndex() / 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 calls CameraAimAssistToggleEvent (getPresetId(), getActionType(), isAimAssistAllowed()).

Implementation & Architecture Overview

  • Plugin entry (Main)

    • Extends PMMP PluginBase and implements Listener.
    • Calls CameraSessionManager::init() and registers event listeners in onEnable().
    • Initializes and syncs both camera presets and aim‑assist presets:
      • On PlayerJoinEvent: creates session + calls CameraPresetRegistry::sendTo($player) and AimAssistPresetRegistry::sendTo($player).
      • On PlayerQuitEvent: removes session.
    • Hooks DataPacketSendEvent and sets the experimental_creator_cameras flag on StartGamePacket / ResourcePackStackPacket Experiments.
  • Session management (CameraSessionManager)

    • Uses an internal \WeakMap<Player, CameraSession> so sessions are automatically cleaned up with the player objects.
    • Provides createSession(), getSession(), removeSession().
  • Timeline (CameraTimeline)

    • Uses the server scheduler and ClosureTask to convert seconds to ticks (20 ticks/sec) and schedule actions.
    • Stores TaskHandler instances in the CameraSession and cancels them on session->stop() to avoid memory leaks and orphaned tasks.
  • Dependencies

    • PocketMine-MP 5.x network packets and camera types.
    • PHP 8 WeakReference / WeakMap.

Examples

Example 1. Simple intro fade + camera move

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();
    }
}

Example 2. Boss cutscene timeline (with fog)

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);
}

Example 3. Top‑down minigame using presets

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();
}

Best Practices & Pitfalls

  • Reuse sessions
    • Camera::of($player) internally caches sessions, so calling it repeatedly is cheap and recommended.
  • Timeline overlapping
    • CameraTimeline::play() calls CameraSession::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 now
    • CameraSession::spline() and CameraTimeline::spline() use CameraSplineBuilder, 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 modify StartGamePacket or ResourcePackStackPacket, be careful about interaction and ordering.
  • 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() return null for unknown preset names.
    • CameraSetBuilder::send() throws \LogicException when no preset was set and \InvalidArgumentException when 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.

About

A high-level camera control API

Resources

Stars

11 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages