Writing Your First Entity
What We’re Building
By the end of this guide you’ll have a working Sentry Turret. It sits still, watches for the player to wander into range and into its line of sight, aims at them, and fires. Shoot it enough and it blows up. Along the way you’ll learn how entities are structured, how to expose properties and wire up inputs and outputs, and how to do range and line of sight checks.
If you later want enemies that patrol, chase, and coordinate as squads, that’s what the built-in NPC System is for. This guide is about building an entity of your own from nothing.
This guide assumes you’ve read Core Concepts and have a working project set up.
Create the File
Create SentryTurret.cs in your project’s Entities/ folder. We’ll write both classes up front:
using Engine;
using Engine.Utils;
using FPSTemplate.Utilities;
using Microsoft.Xna.Framework;
using Rockwall;
using System;
namespace FPSTemplate.Entities;
[EntityDescriptor()]
[ExposeEntityProperty("Detect Range", EntityPropertyType.Float, "How far away it can spot the player, in units.")]
[ExposeEntityProperty("Fire Rate", EntityPropertyType.Float, "Seconds between shots once it has a clear line of sight.")]
[RegisterEntityInputs("Enable", "Disable")]
[RegisterEntityOutputs("OnDestroyed")]
public class SentryTurret : WorldEntity
{
public SentryTurret()
{
Controller = new SentryTurretController();
Bounds = new BoundingBox(-new Vector3(0.5f, 0f, 0.5f), new Vector3(0.5f, 1.2f, 0.5f));
IsSimulated = true;
AxisAlignedBox = true;
RegisterInputLocally("Enable", (s, f) => ((SentryTurretController)Controller).SetEnabled(true));
RegisterInputLocally("Disable", (s, f) => ((SentryTurretController)Controller).SetEnabled(false));
}
}
public class SentryTurretController : EntityController
{
public override void OnSpawn() { }
public override void OnUpdate(GameTime gameTime) { }
public override void OnBeforeRender(GameTime gameTime) { }
public override void OnRender(GameTime gameTime) { }
public override void OnTakeDamage(DamageInfo info) { }
public override void OnDespawn() { }
public void SetEnabled(bool value) { }
}
Two properties, two inputs, one output. Enable and Disable are wired straight to the controller from the constructor.
OnSpawn
Fill in OnSpawn(). This reads back the two exposed properties and sets up the model:
CModelDisplay model;
bool enabled = true;
bool destroyed = false;
int health = 20;
float detectRange;
float fireRate;
float fireTimer;
public override void OnSpawn()
{
detectRange = (float)entity.ReadProperty("Detect Range", EntityPropertyType.Float);
fireRate = (float)entity.ReadProperty("Fire Rate", EntityPropertyType.Float);
model = new CModelDisplay("Models/Props/oildrum.ccmdl");
model.DrawShadow = true;
}
public void SetEnabled(bool value) => enabled = value;
We’re borrowing the oil drum prop model as a placeholder since there’s no dedicated turret model in the SDK, swap the path for your own once you’ve got one. A suspicious, hostile barrel might be even scarier, though.
Detecting the Player
Same pattern for finding nearby entities as anywhere else in the engine, Collision.GetEntitiesInSphere():
Player FindPlayerInRange(float range)
{
var nearby = Collision.GetEntitiesInSphere(entity.Position, range);
foreach (var e in nearby)
{
if (e is Player p) return p;
}
return null;
}
This is worth remembering on its own, it’s the same thing you’d use for area of effect abilities, one enemy alerting others nearby, or any other proximity check.
Line of Sight and Aiming
Being in range isn’t enough, anything solid between the turret and the player shouldn’t count. That’s not just level geometry either, a crate, a door, or another prop sitting in the way should block the shot too, so this needs a physics world cast rather than a BSP-only one. Collision.CastPhysicsWorld() casts against the physics world, and takes a list of bodies to ignore so the turret and the player themselves don’t count as obstructions:
bool HasLineOfSight(Player player)
{
Vector3 toPlayer = player.OrientedBounds.Center - entity.Position;
var ray = new Ray(entity.Position, Vector3.Normalize(toPlayer));
bool blocked = Collision.CastPhysicsWorld(ray, toPlayer.Length(), out _, entity.PhysicsBodyID, player.PhysicsBodyID);
return !blocked;
}
Once we know the player is visible, the turret needs to face them. MathF.Atan2 on the flattened direction gives a yaw angle, which becomes a rotation around the up axis:
void FaceTarget(Vector3 toTarget)
{
float targetAngle = MathHelper.ToDegrees(MathF.Atan2(toTarget.X, toTarget.Z));
Quaternion targetRotation = Quaternion.CreateFromAxisAngle(Vector3.Up, MathHelper.ToRadians(targetAngle));
entity.Rotation = Quaternion.Slerp(entity.Rotation, targetRotation, MainEngine.PreviousFrameDelta * 6f);
}
Slerp instead of snapping straight to the target angle gives the turret a bit of a wind-up as it turns, rather than instantly snapping onto the player.
Firing
The simplest possible version, on a timer, deal damage directly to the player:
void Fire(Player player)
{
player.TakeDamage(new DamageInfo
{
damage = 8,
damageType = 0,
from = entity,
hitLocation = player.OrientedBounds.Center
});
}
This works, but it’s blind in a couple of ways. It fires the instant the cooldown hits zero based on the HasLineOfSight check from earlier in the same frame. And the player has no way to actually perceive the shot. It’s also a lot more boring and drab.
Swap it for Ballistics.ShootBullet(), the same call the FPS template’s own weapons use:
void Fire(Player player)
{
Vector3 muzzle = entity.Position + Vector3.Up * 1f;
Vector3 direction = Vector3.Normalize(player.OrientedBounds.Center - muzzle);
Ballistics.ShootBullet(entity, muzzle, muzzle, direction, 8);
}
This spawns a tracer and hit effects, so the shot is visible and audible instead of just a damage tick. And it nudges physics props along the way with an impulse, same as getting caught in the crossfire from any other weapon in the game. See Weapons for more on how it’s used there.
Now tie it together in OnUpdate:
public override void OnUpdate(GameTime gameTime)
{
if (destroyed || !enabled)
{
model.Update();
return;
}
var player = FindPlayerInRange(detectRange);
if (player != null && HasLineOfSight(player))
{
Vector3 toPlayer = player.OrientedBounds.Center - entity.Position;
FaceTarget(toPlayer);
fireTimer -= MainEngine.PreviousFrameDelta;
if (fireTimer <= 0f)
{
Fire(player);
fireTimer = fireRate;
}
}
model.Update();
}
Rendering
public override void OnBeforeRender(GameTime gameTime)
{
model.RenderShadowTexture(entity);
}
public override void OnRender(GameTime gameTime)
{
model.transform =
Matrix.CreateScale(entity.Scale) *
Matrix.CreateFromQuaternion(entity.Rotation) *
Matrix.CreateTranslation(entity.Position);
model.CheckForLights(entity.OrientedBounds.Center);
model.Draw();
}
Taking Damage and Destruction
public override void OnTakeDamage(DamageInfo info)
{
if (destroyed) return;
health -= info.damage;
if (health <= 0)
{
destroyed = true;
entity.CallOutput("OnDestroyed", entity);
ExplosionManager.CreateExplosion(entity.Position + Vector3.Up * 0.5f, 1.5f, 10f, 1200f, 4f);
EntityManager.DespawnEntity(entity);
}
}
public override void OnDespawn()
{
model.Dispose();
}
The destroyed guard keeps the explosion from firing more than once if the turret takes multiple hits in the same frame. OnDestroyed fires before the explosion so anything listening (a LogicRelay, a counter, a door that unlocks once every turret in a room is down) hears about it immediately.
Where to Go From Here
A few natural next steps: expose Damage as a third property using the same ReadProperty pattern instead of the hardcoded 8. Play a sound the moment it first spots the player with SoundScriptManager.PlaySound(), see Sound. Wire its OnDestroyed output to a LogicRelay so a group of turrets can trigger something together once they’re all down. Add a rotating turret head as a separate attachment so the aiming is visible instead of just implied.
And if what you actually want is something that moves, patrols, takes cover, and reacts as part of a squad, that’s a much bigger job than a single entity file, which is exactly what the NPC System is built to handle.