Class NPC
Represents a non-player character (NPC) in the game. Handles attribute generation, network payloads, and spawning logic.
[RequireComponent(typeof(AIController))]
[RequireComponent(typeof(CharacterPredictionController))]
[RequireComponent(typeof(AbilityController))]
[RequireComponent(typeof(CooldownController))]
[RequireComponent(typeof(BuffController))]
[RequireComponent(typeof(CharacterAttributeController))]
[RequireComponent(typeof(CharacterDamageController))]
[RequireComponent(typeof(FactionController))]
[RequireComponent(typeof(NetworkTransform))]
[RequireComponent(typeof(NetworkObserver))]
public class NPC : BaseCharacter, ICharacter, ISpawnable, ILootableCorpse, IInteractable, ISceneObject
- Inheritance
-
ObjectComponentBehaviourMonoBehaviourNetworkBehaviourNPC
- Implements
- Derived
- Inherited Members
-
NetworkBehaviour.IsSpawnedNetworkBehaviour.ComponentIndexNetworkBehaviour.NetworkObjectNetworkBehaviour.MAXIMUM_NETWORKBEHAVIOURSNetworkBehaviour.UNSET_NETWORKBEHAVIOUR_IDNetworkBehaviour.ToString()NetworkBehaviour.Reset()NetworkBehaviour.IsBehaviourReconcilingNetworkBehaviour.ClearReplicateCache()NetworkBehaviour.CreateReconcile()NetworkBehaviour.Reconcile_Reader<T>(PooledReader, ref T)NetworkBehaviour.OnStartServerCalledNetworkBehaviour.OnStartClientCalledNetworkBehaviour.OnStartNetwork()NetworkBehaviour.OnStopNetwork()NetworkBehaviour.OnStopServer()NetworkBehaviour.OnOwnershipServer(NetworkConnection)NetworkBehaviour.OnSpawnServer(NetworkConnection)NetworkBehaviour.OnDespawnServer(NetworkConnection)NetworkBehaviour.OnStartClient()NetworkBehaviour.OnStopClient()NetworkBehaviour.OnOwnershipClient(NetworkConnection)NetworkBehaviour.ClearBuffedRpcs()NetworkBehaviour.ExcludeOwnerFromUnbufferedObserversRpcsNetworkBehaviour.IsClientOnlyNetworkBehaviour.IsServerOnlyNetworkBehaviour.IsHostNetworkBehaviour.IsClientNetworkBehaviour.IsServerNetworkBehaviour.IsDeinitializingNetworkBehaviour.NetworkManagerNetworkBehaviour.ServerManagerNetworkBehaviour.ClientManagerNetworkBehaviour.ObserverManagerNetworkBehaviour.TransportManagerNetworkBehaviour.TimeManagerNetworkBehaviour.SceneManagerNetworkBehaviour.PredictionManagerNetworkBehaviour.RollbackManagerNetworkBehaviour.NetworkObserverNetworkBehaviour.IsClientInitializedNetworkBehaviour.IsClientStartedNetworkBehaviour.IsClientOnlyInitializedNetworkBehaviour.IsClientOnlyStartedNetworkBehaviour.IsServerInitializedNetworkBehaviour.IsServerStartedNetworkBehaviour.IsServerOnlyInitializedNetworkBehaviour.IsServerOnlyStartedNetworkBehaviour.IsHostInitializedNetworkBehaviour.IsHostStartedNetworkBehaviour.IsOfflineNetworkBehaviour.IsNetworkedNetworkBehaviour.GetIsNetworked()NetworkBehaviour.IsManagerReconcilingNetworkBehaviour.ObserversNetworkBehaviour.IsOwnerNetworkBehaviour.IsControllerNetworkBehaviour.HasAuthorityNetworkBehaviour.OwnerNetworkBehaviour.OwnerIdNetworkBehaviour.ObjectIdNetworkBehaviour.LocalConnectionNetworkBehaviour.OwnerMatches(NetworkConnection)NetworkBehaviour.Despawn(GameObject, DespawnType?)NetworkBehaviour.Despawn(NetworkObject, DespawnType?)NetworkBehaviour.Despawn(DespawnType?)NetworkBehaviour.Spawn(GameObject, NetworkConnection, Scene)NetworkBehaviour.Spawn(NetworkObject, NetworkConnection, Scene)NetworkBehaviour.RemoveOwnership()NetworkBehaviour.GiveOwnership(NetworkConnection)NetworkBehaviour.GetInstance<T>()NetworkBehaviour.TryRegisterInstance<T>(T)NetworkBehaviour.UnregisterInstance<T>()NetworkBehaviour.CanLog(LoggingType)MonoBehaviour.IsInvoking()MonoBehaviour.CancelInvoke()MonoBehaviour.StopCoroutine(Coroutine)MonoBehaviour.StopAllCoroutines()MonoBehaviour.destroyCancellationTokenMonoBehaviour.useGUILayoutMonoBehaviour.didStartMonoBehaviour.didAwakeMonoBehaviour.runInEditModeBehaviour.enabledBehaviour.isActiveAndEnabledComponent.GetComponent<T>()Component.TryGetComponent<T>(out T)Component.GetComponentInChildren<T>()Component.GetComponentsInChildren<T>()Component.GetComponentInParent<T>()Component.GetComponentsInParent<T>()Component.GetComponents<T>()Component.GetComponentIndex()Component.CompareTag(TagHandle)Component.transformComponent.transformHandleComponent.gameObjectComponent.tagObject.GetEntityId()Object.GetInstanceID()Object.GetHashCode()Object.InstantiateAsync<T>(T)Object.InstantiateAsync<T>(T, Transform)Object.InstantiateAsync<T>(T, Vector3, Quaternion)Object.InstantiateAsync<T>(T, Transform, Vector3, Quaternion)Object.Instantiate(Object, Vector3, Quaternion)Object.Instantiate(Object, Vector3, Quaternion, Transform)Object.Instantiate(Object)Object.Instantiate(Object, Scene)Object.Instantiate<T>(T, InstantiateParameters)Object.Instantiate<T>(T, Vector3, Quaternion, InstantiateParameters)Object.Instantiate(Object, Transform)Object.Instantiate<T>(T)Object.Instantiate<T>(T, Vector3, Quaternion)Object.Instantiate<T>(T, Vector3, Quaternion, Transform)Object.Instantiate<T>(T, Transform)Object.Destroy(Object)Object.DestroyImmediate(Object)Object.DontDestroyOnLoad(Object)Object.DestroyObject(Object)Object.FindObjectsOfType<T>()Object.FindObjectsByType<T>(FindObjectsSortMode)Object.FindObjectsByType<T>(FindObjectsInactive, FindObjectsSortMode)Object.FindObjectOfType<T>()Object.FindFirstObjectByType<T>()Object.FindAnyObjectByType<T>()Object.FindFirstObjectByType<T>(FindObjectsInactive)Object.FindAnyObjectByType<T>(FindObjectsInactive)Object.FindObjectsByType<T>()Object.FindObjectsByType<T>(FindObjectsInactive)Object.nameObject.hideFlags
- Extension Methods
Fields
Abilities
Ability templates this NPC can use. Populated in the inspector. Each template is learned as an Ability instance during OnStartServer(), before clients receive the spawn payload.
[Header("Abilities")]
[Tooltip("Ability templates this NPC knows. Learned on server start.")]
public List<AbilityTemplate> Abilities
Field Value
AttributeBonuses
Database of attribute bonuses for this NPC.
public NPCAttributeDatabase AttributeBonuses
Field Value
CorpseDecayDuration
[Header("Corpse Decay")]
[Tooltip("Seconds the corpse remains visible after death before returning to the object pool.")]
public float CorpseDecayDuration
Field Value
CorpseInteractionRange
How close a player must stand to loot this NPC's corpse.
[Tooltip("Distance within which a player may loot this NPC's corpse.")]
public float CorpseInteractionRange
Field Value
EmptyCorpseDecayDuration
Seconds an empty corpse remains before returning to the object pool.
[Tooltip("Seconds an empty corpse remains before returning to the pool. Only ever shortens the decay, never extends it.")]
[Min(0)]
public float EmptyCorpseDecayDuration
Field Value
Remarks
A body with nothing on it is scenery. The full decay duration exists to give the people who earned the kill time to walk over and take their loot, and none of that applies once there is nothing left to take — or once it turns out there never was, which is the case for every NPC killed by another NPC, by the environment, or by a player with no loot table configured. Left on the full timer those bodies accumulate: a busy zone ends up carpeted in corpses that no one can interact with, each still spawned, still observed by every client in range, and still holding its spawner's slot.
Only ever shortens a corpse's remaining life, never extends it, so setting this longer than CorpseDecayDuration simply has no effect.
Keep it comfortably longer than the death animation, or bodies will pop out of the world mid-collapse.
IsCharmable
If true, this NPC can be charmed by players.
public bool IsCharmable
Field Value
LootTable
[Header("Loot")]
[Tooltip("What this NPC's corpse may hold. Rolled once, on the server, at the moment of death.")]
public LootTableTemplate LootTable
Field Value
MaximumRespawnTime
Longest time in seconds before this NPC respawns after its corpse decays.
[Tooltip("Longest time in seconds before this NPC respawns after its corpse decays. A spawner may override it.")]
[Min(0)]
public float MaximumRespawnTime
Field Value
Remarks
The prefab is the right home for these because respawn cadence is a property of the creature, not of the patch of ground it stands on: a wolf should come back at wolf pace wherever it is placed, and a rare spawn should stay rare at every spawner that can produce it. Before this, the values lived only on NPCSpawnableSettings and defaulted to zero — so any spawner whose author did not fill them in respawned its creatures instantly, forever.
A spawner can still override the pair for a specific placement — see NPCSpawnableSettings.MinimumRespawnTime.
MinimumRespawnTime
[Header("Respawn")]
[Tooltip("Shortest time in seconds before this NPC respawns after its corpse decays. A spawner may override it.")]
[Min(0)]
public float MinimumRespawnTime
Field Value
Properties
HasLoot
True while the corpse still holds an item or any currency.
public bool HasLoot { get; }
Property Value
InteractRateLimit
Minimum milliseconds between interactions with this object from one character.
public double InteractRateLimit { get; }
Property Value
IsCorpse
True while this NPC is a lootable corpse.
public bool IsCorpse { get; }
Property Value
Remarks
Answers from whichever source is authoritative for the peer asking. The server owns FishMMO.Shared.NPC.isCorpse outright; a client has no copy of it and reads the replicated dead flag instead, which arrives live by observers RPC and in the spawn payload for anyone who turns up later. Without the client arm this property is simply false on every client, and the corpse is never offered as an interaction target at all.
LootCurrency
Currency remaining on the corpse.
public long LootCurrency { get; }
Property Value
LootItems
The corpse's item slots. Emptied slots read null and keep their index.
public IReadOnlyList<Item> LootItems { get; }
Property Value
LootViewers
Connections currently viewing this corpse's loot.
public IReadOnlyCollection<NetworkConnection> LootViewers { get; }
Property Value
- IReadOnlyCollection<NetworkConnection>
ObjectSpawner
Reference to the spawner that created this NPC.
public ObjectSpawner ObjectSpawner { get; set; }
Property Value
OnInteractTriggers
ECA triggers invoked server-side when a player successfully interacts with this object. Configure these in the Inspector to define per-object interaction behaviour without code.
public List<Trigger> OnInteractTriggers { get; }
Property Value
RNG
Exposes the seeded RNG for deterministic AI decisions. All AI subsystems should use this instead of Shared so that behaviour is reproducible given the same seed.
public DeterministicRNG RNG { get; }
Property Value
SpawnableSettings
Settings used when spawning this NPC.
public SpawnableSettings SpawnableSettings { get; set; }
Property Value
Title
The interaction title. Empty while alive, so a living NPC is not advertised as interactable and its world label is left to the naming components.
public virtual string Title { get; }
Property Value
TitleColor
The colour of the corpse title in the world UI.
public virtual Color TitleColor { get; }
Property Value
- Color
Methods
AddLootViewer(NetworkConnection)
Records that a connection has the loot window open on this corpse.
public void AddLootViewer(NetworkConnection connection)
Parameters
connectionNetworkConnectionThe viewing connection.
CanInteract(IPlayerCharacter)
Returns true when the given player may open this NPC's loot window.
public virtual bool CanInteract(IPlayerCharacter character)
Parameters
characterIPlayerCharacter
Returns
Remarks
Runs on both sides and answers differently on each, deliberately. The client knows only that the NPC is dead, which is all it needs to decide whether to send a request; the server additionally holds the contributor snapshot and is the only place eligibility is actually enforced. A client that lies here gets a refusal, not loot.
Pure, like every other CanInteract(IPlayerCharacter). The corpse rate limit is spent through TryConsumeInteractRateLimit(IPlayerCharacter) instead.
Despawn()
Enters corpse state or returns to pool. On first call after death, the NPC becomes a corpse (visible, immobile, immortal, and lootable) for CorpseDecayDuration seconds. After the timer expires, the object is returned to FishNet's pool for reuse.
public virtual void Despawn()
Remarks
Called from the server's OnKilled subscriber. Everything it does is server state;
clients learn that the NPC is a corpse from IsDead, which
travels both live (an observers RPC from the damage controller) and in the spawn payload
for anyone who arrives afterwards.
ExecuteOnInteract(EventData)
Fires all OnInteractTriggers with the provided event data. Called by FishMMO.Server.Implementation.World.SceneServer.Interactable.InteractableSystem after server-side validation succeeds.
public bool ExecuteOnInteract(EventData eventData)
Parameters
eventDataEventDataContext for this interaction (initiator, interactable reference, etc.).
Returns
- bool
True when at least one trigger was fired.
Remarks
Unlike every other interactable, an NPC with no triggers is not a misconfiguration: looting is wired directly into the interaction handler precisely so that a creature with an empty list is still lootable. Triggers here are the optional extras — an achievement, a quest update, a line of dialogue on the body — so an empty list is silent.
InRange(Transform)
Returns true if the specified transform is within interaction range of this object.
public bool InRange(Transform transform)
Parameters
transformTransformThe transform to check range against.
Returns
- bool
True if in range, false otherwise.
IsEligibleLooter(long)
Returns true if the given character earned loot rights to this corpse.
public bool IsEligibleLooter(long characterID)
Parameters
characterIDlongThe character ID to test.
Returns
OnAwake()
Called when the NPC is awakened. Handles name cleanup and registration.
public override void OnAwake()
OnDestroying()
Called when the NPC is destroyed. Unregisters from the scene object registry.
public override void OnDestroying()
OnStartServer()
Called when the server starts for this NPC. Runs on every spawn including pool reuse. Re-rolls the seed, RNG, gender, and name. Then applies attribute bonuses and learns abilities. Spawner overrides (AttributeBonuses, CorpseDecayDuration) are injected before this runs.
public override void OnStartServer()
Remarks
Deliberately not wrapped in #if UNITY_SERVER. FishNet only calls this on a peer
that is actually running a server, so the compile-time gate bought nothing — and it cost
a great deal: in an editor or host build (where UNITY_SERVER is undefined) the override
did not exist, so no NPC ever rolled its RNG, applied its attribute bonuses, or learned
a single ability. NPC combat could not be exercised anywhere except a dedicated server
build.
OnValidate()
Keeps the respawn range orderable in the inspector.
protected virtual void OnValidate()
Remarks
An inverted range is not merely odd: the RNG returns the minimum whenever
min >= max, so a prefab authored 60-to-30 would silently respawn at a fixed
60 seconds and never vary. Clamping here makes that impossible to author by accident.
ReadPayload(NetworkConnection, Reader)
Reads the NPC's payload from the network, including ID and attribute seed. Applies attributes and sets up model.
public override void ReadPayload(NetworkConnection connection, Reader reader)
Parameters
connectionNetworkConnectionThe network connection.
readerReaderThe network reader.
RemoveLootViewer(NetworkConnection)
Records that a connection has closed the loot window on this corpse.
public void RemoveLootViewer(NetworkConnection connection)
Parameters
connectionNetworkConnectionThe connection that closed it.
ResetState(bool)
Resets the NPC's state for object pool reuse. Clears RNG, spawner references, and client-side tracking.
public override void ResetState(bool asServer)
Parameters
asServerboolWhether the reset is performed on the server.
ReturnLootCurrency(long)
Puts currency back onto the corpse after a failed grant.
public void ReturnLootCurrency(long amount)
Parameters
amountlongThe amount to restore.
ReturnLootItem(Item, int)
Puts an item back into a slot it was taken from.
public bool ReturnLootItem(Item item, int slot)
Parameters
Returns
- bool
True when the item was restored.
Remarks
The take must be undoable because granting can fail after the corpse has already given the item up — a full inventory being the ordinary case. Without a way back, that item is destroyed rather than left on the corpse.
ReturnToPool()
Returns the NPC to the object pool immediately. Called when the corpse decay timer expires or on server shutdown.
public void ReturnToPool()
Remarks
Routes through the owning ObjectSpawner so a respawn is scheduled, and despawns directly when there is no spawner. The fallback matters: an NPC placed by script or adopted rather than spawned has no spawner, and the null-conditional this replaced meant such an NPC's corpse never decayed at all — it sat in the world as an immortal, AI-disabled, permanently lootable body that nothing would ever collect.
TryConsumeInteractRateLimit(IPlayerCharacter)
Spends the character's interact rate-limit, returning false when it is still cooling.
public bool TryConsumeInteractRateLimit(IPlayerCharacter character)
Parameters
characterIPlayerCharacterThe interacting character.
Returns
- bool
True when the limiter was free and has now been spent.
Remarks
Separate from CanInteract(IPlayerCharacter) so a caller that only needs to validate — the quest system, which already holds its own ingress guard — does not consume a budget belonging to the interaction path.
TryTakeLootCurrency(long, out long)
Takes up to maximum currency from the corpse.
public bool TryTakeLootCurrency(long maximum, out long amount)
Parameters
maximumlongThe most the caller can accept. Must be positive.
amountlongReceives the amount actually taken.
Returns
- bool
True when any currency was taken.
Remarks
Capped rather than all-or-nothing because the destination has a ceiling: a character's currency is an int, and a taker already near that ceiling must be able to take what fits and leave the rest on the body rather than either overflowing or being refused.
TryTakeLootItem(int, out Item)
Removes the item in the given slot and hands it to the caller.
public bool TryTakeLootItem(int slot, out Item item)
Parameters
Returns
- bool
True when a item was removed.
WritePayload(NetworkConnection, Writer)
Writes the NPC's payload to the network, including ID and attribute seed. Ensures deterministic attribute generation on clients.
public override void WritePayload(NetworkConnection connection, Writer writer)
Parameters
connectionNetworkConnectionThe network connection.
writerWriterThe network writer.
Events
OnCorpseExpired
Raised on the server when the corpse is about to leave the world, so open loot windows can be closed before the scene object ID they refer to stops resolving.
public event Action<ILootableCorpse> OnCorpseExpired