Skip to main content
In this tutorial we’ll build a complete ESP from scratch. By the end you’ll have animated corner boxes, health bars with damage indicators, name tags, and visibility-aware coloring - all running in real-time.
Final ESP result

What we're building: corner box ESP with health bars and name tags.

What you’ll learn

1

Project setup

Import modules and set up the render loop.
2

Bounding box

Calculate screen-space bounding boxes from collision data.
3

Box rendering

Draw outlined boxes with corner accents.
4

Health bars

Animated health bars with gradient colors and damage indicators.
5

Name tags & visibility

Player names with text shadows and visibility-based coloring.

Step 1 - Project setup

Every ESP starts the same way: import what you need, hook into the render event, and grab the local player.
getPlayers({ skipLocal: true }) returns all player pawns except your own. We skip dead players early with m_iHealth <= 0.

Step 2 - Bounding box calculation

The naïve approach is to hardcode a height offset (origin.z + 72), but that breaks for crouching players and non-standard models. Instead, we use the entity’s collision bounds - m_pCollision gives us the real mins/maxs of the hitbox. We project all 8 corners of the 3D box to screen space and find the tightest 2D rectangle that contains them:
The bitmask trick i & 1, i & 2, i & 4 generates all 8 combinations of min/max for x, y, z - one for each corner of the box.
If any corner is behind the camera, worldToScreen returns success: false and we bail out - drawing a partial box looks worse than drawing nothing. Let’s plug it in:

Step 3 - Box rendering

A single Render.rect looks flat. For a polished look we’ll draw three layers - dark outer outline, colored main box, and dark inner outline - plus animated corner accents.

Basic triple-outline box

This alone already looks much better than a single rectangle. The dark outlines create depth and make the box readable against any background.

Corner accents

Corner-style ESP draws short lines at each corner instead of a full rectangle. We’ll animate their length with a sine wave for a subtle pulsing effect:
Adding enemy.index to the sine offset makes each player’s corners pulse independently - otherwise all boxes breathe in sync which looks unnatural.

Step 4 - Health bars

A health bar sits to the left of the bounding box. We’ll add:
  • A dark background with outline
  • A color-gradient fill (green → red)
  • Smooth animation when health changes
  • A white “damage indicator” showing where health used to be

Per-player state

To animate smoothly, we need to remember each player’s previous health value between frames:

Drawing the bar

The Math.lerp with frameTime * 10 creates an exponential decay animation - the bar moves fast at first then slows down as it approaches the target value.

Step 5 - Name tags & visibility

Eye position helper

For accurate visibility checks we need the player’s eye position, not just their feet. The view offset is stored as a CNetworkViewOffsetVector - a packed struct that comes back as raw bytes. We read the floats manually:
m_vecViewOffset is a CNetworkViewOffsetVector with 16 bytes of padding, then three CNetworkedQuantizedFloat structs at offsets 0x10, 0x18, 0x20. Each starts with a float we can read directly.

Visibility-aware box color

Instead of a static red box, we’ll smoothly transition between green (visible) and red (hidden):
Now replace the hardcoded Color.red() in the box and corner accent drawing with boxColor.

Name tags with shadow

Use Render.calcTextSize to measure the text and center it above the box. A text shadow (same text drawn 1px offset in black) makes names readable against any background:

Final result

Here’s everything put together - a complete, working ESP script:

Next steps

Now that you have a working ESP, try extending it:
  • Weapon name - read enemy.m_hActiveWeapon and display the weapon class below the box
  • Distance - show distance in meters between you and each player
  • Snaplines - draw lines from the bottom of your screen to each enemy
  • Team filtering - compare m_iTeamNum to only draw enemies, not teammates