How Yaw Rotation Works in Bedrock
Every directional display in Minecraft Bedrock relies on reading the player's horizontal head rotation, known as yaw. In Bedrock's coordinate system, yaw is measured in degrees ranging from -180.0 to 180.0. Understanding where the four cardinal directions fall on this circle is the foundation of any compass recipe.
South sits at exactly 0 degrees. As the player turns clockwise toward the West, the yaw value increases positively up to 90 degrees. Continuing clockwise toward North pushes the value up to 180 degrees. Because the scale wraps around at the northern meridian, turning slightly past North toward the East flips the value to -180 degrees. From there, continuing clockwise brings the player through East at -90 degrees and back to South at 0.
When you test for these angles in commands using the ry (rotation yaw maximum) and rym (rotation yaw minimum) target selector arguments, you must account for this wrap-around at North. Seven of the eight standard compass slices can be checked with a single continuous range where rym is less than or equal to ry. Due North straddles the 180 and -180 boundary, meaning a pure command setup must split the North check into two separate commands or rely on a script to normalize the angle first.
| Direction | Exact Center Yaw | Selector Range (rym to ry) | Coordinate Axis |
|---|---|---|---|
| South (S) | 0° | rym=-22, ry=22 | Positive Z (+Z) |
| Southwest (SW) | 45° | rym=23, ry=67 | -X / +Z |
| West (W) | 90° | rym=68, ry=112 | Negative X (-X) |
| Northwest (NW) | 135° | rym=113, ry=157 | -X / -Z |
| North (N) | 180° / -180° | rym=158, ry=180 AND rym=-180, ry=-158 | Negative Z (-Z) |
| Northeast (NE) | -135° | rym=-157, ry=-113 | +X / -Z |
| East (E) | -90° | rym=-112, ry=-68 | Positive X (+X) |
| Southeast (SE) | -45° | rym=-67, ry=-23 | +X / +Z |
Pure Command Recipe: 8-Point Actionbar Compass
If your add-on does not use scripts, you can drive an 8-point compass entirely from a ticking mcfunction file. The strategy is to run nine titleraw commands every tick—one for each of the seven contiguous directional wedges, and two to cover the split wedge at North. Each command uses @a[rym=..., ry=...] so that every player in the world sees the string matching their own camera angle.
Using titleraw instead of the plain title command lets you embed formatting codes and scoreboard values later if you want to append coordinates or a timer next to the compass. The example below uses color formatting codes (§c for red on North, §f for white on active directions, and §7 for gray on flanking directions) to simulate a three-slot viewport that shifts as the player turns.
To keep the compass responsive without flooding the command engine in multiplayer, register the function in BP/functions/tick.json. Because tick.json executes 20 times per second, the actionbar text will never fade out, and transitions between the 45-degree slices will feel immediate to the player.
# South (-22 to 22)
titleraw @a[rym=-22,ry=22] actionbar {"rawtext":[{"text":"§7[ SE | §f§lS§r§7 | SW ]"}]}
# Southwest (23 to 67)
titleraw @a[rym=23,ry=67] actionbar {"rawtext":[{"text":"§7[ S | §f§lSW§r§7 | W ]"}]}
# West (68 to 112)
titleraw @a[rym=68,ry=112] actionbar {"rawtext":[{"text":"§7[ SW | §f§lW§r§7 | NW ]"}]}
# Northwest (113 to 157)
titleraw @a[rym=113,ry=157] actionbar {"rawtext":[{"text":"§7[ W | §f§lNW§r§7 | §cN§7 ]"}]}
# North Split 1 (158 to 180)
titleraw @a[rym=158,ry=180] actionbar {"rawtext":[{"text":"§7[ NW | §c§lN§r§7 | NE ]"}]}
# North Split 2 (-180 to -158)
titleraw @a[rym=-180,ry=-158] actionbar {"rawtext":[{"text":"§7[ NW | §c§lN§r§7 | NE ]"}]}
# Northeast (-157 to -113)
titleraw @a[rym=-157,ry=-113] actionbar {"rawtext":[{"text":"§7[ §cN§7 | §f§lNE§r§7 | E ]"}]}
# East (-112 to -68)
titleraw @a[rym=-112,ry=-68] actionbar {"rawtext":[{"text":"§7[ NE | §f§lE§r§7 | SE ]"}]}
# Southeast (-67 to -23)
titleraw @a[rym=-67,ry=-23] actionbar {"rawtext":[{"text":"§7[ E | §f§lSE§r§7 | S ]"}]}Registering the Function Loop
For the mcfunction file above to run automatically, reference its path (without the .mcfunction extension) inside BP/functions/tick.json. When the world loads, Bedrock runs every listed function once per game tick.
If you only want the compass to appear when a player holds a specific item or wears a custom helmet, add a tag filter such as tag=has_compass to every @a selector in compass_hud.mcfunction. You can then grant or revoke that tag from a separate controller function or script.
{
"values": [
"compass_hud"
]
}Script API Recipe: Smooth Scrolling Tape Compass
While the 9-command mcfunction is reliable, jumping in 45-degree increments feels stiff compared to modern games that use a horizontal scrolling tape at the top or bottom of the screen. Writing a 360-command mcfunction to cover every single degree is bad practice and hurts server tick performance. Instead, use the @minecraft/server Script API to read the exact float value from player.getRotation().y and slice a continuous text ribbon.
In the script below, we build a single character string representing a full 360-degree horizon, then repeat it three times so we can cleanly extract a substring window around the player's current yaw without worrying about array out-of-bounds errors at the 0/360 seam. We convert Bedrock's -180 to 180 yaw into a standard 0 to 360 bearing where North is 0, East is 90, South is 180, and West is 270.
Calling player.onScreenDisplay.setActionBar() updates the HUD directly from JavaScript without spawning command output log messages or requiring command blocks to be enabled in the world settings. Running the interval every 2 ticks (10 times per second) gives smooth motion while keeping CPU usage negligible.
import { world, system } from "@minecraft/server";
// A 72-character ribbon where each character represents 5 degrees (72 * 5 = 360).
// Index 0 corresponds to North (0 degrees).
const COMPASS_RIBBON = "N · · · · NE· · · · E · · · · SE· · · · S · · · · SW· · · · W · · · · NW· · · · ";
const TRIPLE_RIBBON = COMPASS_RIBBON + COMPASS_RIBBON + COMPASS_RIBBON;
const VIEWPORT_HALF_WIDTH = 12;
system.runInterval(() => {
for (const player of world.getAllPlayers()) {
const rot = player.getRotation();
// Convert Bedrock yaw (-180 at North, 0 at South) to 0-360 compass bearing (0 at North, 90 at East)
let bearing = (rot.y + 180) % 360;
if (bearing < 0) {
bearing += 360;
}
// Map the 0-360 bearing to our 72-character ribbon
const centerIndex = Math.round(bearing / 5) % 72;
const offsetIndex = centerIndex + 72;
const leftSlice = TRIPLE_RIBBON.substring(offsetIndex - VIEWPORT_HALF_WIDTH, offsetIndex);
const centerChar = TRIPLE_RIBBON.substring(offsetIndex, offsetIndex + 1);
const rightSlice = TRIPLE_RIBBON.substring(offsetIndex + 1, offsetIndex + 1 + VIEWPORT_HALF_WIDTH);
const formattedDegree = Math.floor(bearing).toString().padStart(3, "0");
const hudText = `§8[ §7${leftSlice}§e§l${centerChar}§r§7${rightSlice} §8] §f${formattedDegree}°`;
player.onScreenDisplay.setActionBar(hudText);
}
}, 2);Adding Dynamic Waypoint Markers to the Tape
Once your compass is driven by a script, you can superimpose objective markers—such as a quest target, a bed, or a capture point—directly onto the scrolling ribbon. To calculate the angle from the player to a target coordinate, subtract the player's X and Z coordinates from the target's X and Z coordinates, then pass those deltas into Math.atan2().
Because Minecraft's Z axis points South and X axis points East, computing Math.atan2(-dx, dz) in radians and converting to degrees gives a value that matches Bedrock's native -180 to 180 yaw directly. Once you convert both the player's yaw and the target angle into 0-to-72 ribbon indices, you can replace the dot character at the waypoint's index with a colored symbol before slicing the string.
This technique also lets you store per-player waypoint coordinates using player.setDynamicProperty(), meaning every player in a multiplayer session can see their own personal quest marker on their compass ribbon simultaneously.
import { world, system } from "@minecraft/server";
const BASE_RIBBON = "N · · · · NE· · · · E · · · · SE· · · · S · · · · SW· · · · W · · · · NW· · · · ";
const TARGET_POS = { x: 0, z: 0 };
function yawToBearing(yaw) {
let b = (yaw + 180) % 360;
return b < 0 ? b + 360 : b;
}
system.runInterval(() => {
for (const player of world.getAllPlayers()) {
const loc = player.location;
const rot = player.getRotation();
// Calculate yaw from player to target (0, 0)
const dx = TARGET_POS.x - loc.x;
const dz = TARGET_POS.z - loc.z;
const targetYaw = Math.atan2(-dx, dz) * (180 / Math.PI);
const playerBearing = yawToBearing(rot.y);
const targetBearing = yawToBearing(targetYaw);
const targetIndex = Math.round(targetBearing / 5) % 72;
// Inject a diamond marker into a copy of the ribbon at the target's bearing
const ribbonChars = BASE_RIBBON.split("");
ribbonChars[targetIndex] = "♦";
const customRibbon = ribbonChars.join("");
const triple = customRibbon + customRibbon + customRibbon;
const centerIdx = (Math.round(playerBearing / 5) % 72) + 72;
const view = triple.substring(centerIdx - 12, centerIdx + 13);
const dist = Math.round(Math.hypot(dx, dz));
player.onScreenDisplay.setActionBar(`§7< ${view} > §a♦ ${dist}m`);
}
}, 2);Which versions this applies to
The pure command and mcfunction recipe works in all Bedrock versions from 1.16.0 onward with no experimental toggles required. The Script API recipe uses stable APIs (world.getAllPlayers, getRotation, onScreenDisplay.setActionBar, system.runInterval) available in @minecraft/server 1.4.0 and newer (Bedrock 1.20.10+) without needing Beta APIs enabled.
When it does not work
| What you see | Why | The fix |
|---|---|---|
| Compass stops updating when the player looks due North, or flickers an old direction. | You used a single selector like @a[rym=158,ry=-158] for North. In Bedrock selectors, rym (minimum) cannot be numerically larger than ry (maximum); the selector simply matches zero entities. | Split the North check into two separate commands: one for positive angles (@a[rym=158,ry=180]) and one for negative angles (@a[rym=-180,ry=-158]). |
| Syntax error: Unexpected ":" at "actionbar >>{"rawtext"<<" in the function log. | You used the regular /title command instead of /titleraw while passing a JSON rawtext object, or you have unescaped quotes inside your JSON string. | Change the command from title to titleraw whenever you pass {"rawtext":[...]}, as the standard title command expects plain unquoted text. |
| The actionbar compass jitter-shifts left and right as the ribbon scrolls in the Script API version. | Minecraft's default font is proportional, meaning characters like 'W' and 'M' are wider in pixels than '.' or 'I', and bold formatting (§l) adds extra pixel width to the center character. | Avoid toggling bold (§l) on single characters inside a scrolling substring, and use middle dots (·) or dashes (-) which have closer pixel widths to uppercase letters than standard periods. |
Reference, straight from the game files
Read out of the game itself, so the ids match the version you are building for.
