Entities

From Pragma
Jump to: navigation, search

Entities

Entities are objects that are part of the game world, or manipulate it in some manner. Some examples are: The players, props, NPCs, the skybox, light sources and logical entities like triggers, constraints, buttons, etc. Even the world is an entity.

Conventional entity systems are often based on an inheritance model, where all entity classes are derived from base classes. This could look something like this: Entity inheritance model.png

This system has several problems:

  • To extend an entity, you have to modify its source code. For example, if you want an existing weapon to cause explosions whenever the player hits something with it, you either have to include it in the weapon's code, or find some other way to attach your code to it.
  • You can't have entities that are derived from several base classes. If you want a vehicle, which is also a NPC (ai-controlled), you'll have to create two separate entities, one for the vehicle-system and one for the AI-system.
  • Entities have data stored that they might never need in the first place. A light entity for example probably won't ever need a velocity. This results in a lot of wasted space, and larger memory allocations per entity.

For these reasons, pragma uses an Entity–component–system (ECS) instead. In a nutshell this means that all entities are inherently the same, they only differ by their classname, their components, and their network type (clientside/serverside/shared). Entities have no logic of their own whatsoever! To make it easier, you can think of the classname as the entity's ID, and the entity itself just being a composition of components.

A player entity, for example, would have the following components (among others): player, character, physics, health, render, damageable, transform, velocity, sound_emitter, submergible, observable, health, flammable, name, gravity, logic.

  • Remove the damageable component, and the player will not be able to take damage anymore.
  • Remove the gravity component, and the player will not be affected by gravity anymore (essentially become weightless).
  • Remove the sound_emitter component, and the player will become silent (and cannot emit sounds anymore).
  • Add an ai component, and the player would become a NPC (while still being a player).
  • Remove the player component, and the player will essentially cease to be a player.

You get the gist of it. The components perform the actual logic of the entities. Each component represents an independent sub-system, however some components rely on others, or communicate with other components. The damageable component, for example, is dependent on the health component. Without it, it cannot work properly. Note that components are never derived from each other!

Components can be added and removed from entities dynamically, and a component always belongs to exactly one entity. Technically an entity can have multiple components of the same time, however this should be avoided. Components can communicate with each other through various means, and they can be either clientside, serverside, or both (networked). Networked components require the entity to be shared.

Entity component model.png

Working with entities

To work with an entity, you need a handle to it. There are a number of functions that return handles to entities depending on what you need, the easiest being ents.get_all, which simply returns a list of all entities currently active in the game (note that this includes entities which haven't been spawned yet!):

-- Print all entities to the console
for _,ent in ipairs(ents.get_all()) do
	print(ent)
end

Since entities can potentially be removed and become invalid at any time, you should always make sure the entity handle you're working with is valid:

local ent = ents.get_local_player() -- Returns nil if there is no local player
if(util.is_valid(ent)) then
	-- Do stuff
end

util.is_valid(o) is simply a shortcut for (o ~= nil and o:IsValid()), which can be used for any handle type (not just entity handles). Most engine/game functions return nil if the entity isn't valid, which is why it's important to check for nil.

Iterators

ents.get_all is a quick and dirty way to retrieve all entities, but it's slow and should be avoided in functions that are called continuously, or in cases where you only need specific types of entities. The preferred way of retrieving entity handles is to use entity iterators instead. The simplest form of such an iterator looks like this:

local entIt = ents.iterator()
local ent0 = entIt() -- First entity
local ent1 = entIt() -- Second entity
-- etc.

-------------------

-- Iterate through all (spawned) entities:
for ent in ents.iterator() do
	 -- Do stuff
end

-------------------

-- Retrieve all entities from an iterator:
local tEnts = ents.get_all(ents.iterator())

Iterators can have additional filters attached to them, in cases where you only want specific entity types:

-- Retrieve all entities that are spawned and have a player component attached to them
local tPlayers = ents.get_all(ents.iterator(bit.bor(ents.ITERATOR_FILTER_DEFAULT,ents.ITERATOR_FILTER_BIT_PLAYER)))

-- Retrieve ALL entities (including entities that haven't been spawned yet). This is equivalent to just calling ents.get_all() regularly
local tEnts = ents.get_all(ents.iterator(ents.ITERATOR_FILTER_ANY))

-- Retrieve all entites that are spawned, and which are clientside or serverside only (depending on how this script was run)
local tLocalEnts = ents.get_all(ents.iterator(bit.bor(ents.ITERATOR_FILTER_DEFAULT,ents.ITERATOR_FILTER_BIT_INCLUDE_NETWORK_LOCAL)))

-- Returns all entities that are spawned and have the class "npc_zombie"
local tEntsOfClass = ents.get_all(ents.iterator({ents.IteratorFilterClass("npc_zombie")}))

-- Returns all entities that have both the "flammable" and "transform" components, and which are located within 512 units around the world origin
local tEntsFiltered = ents.get_all(ents.iterator({ents.IteratorFilterComponent(ents.COMPONENT_FLAMMABLE),ents.IteratorFilterComponent("transform"),ents.IteratorFilterSphere(Vector(0,0,0),512)}))

-- Returns all map entities that are located in the specified box bounds
local tEntsMap = ents.get_all(ents.iterator(bit.bor(ents.ITERATOR_FILTER_DEFAULT,ents.ITERATOR_FILTER_BIT_MAP_ENTITY),{ents.IteratorFilterBox(Vector(-100,-100,-100),Vector(100,100,100))}))

Create entities

To create a new entity, you can use ents.create(className). Note that this will only allocate the entity, but it won't actually exist in the world yet. To do so, you need to spawn it as well:

local ent = ents.create("npc_zombie")
if(ent ~= nil) then
	ent:Spawn()
end

If the entity is shared, and it was created serverside, spawning it will also cause it to be created and spawned clientside.

You can also create new entities by using the ent_create <className> console command, which will immediately spawn the entity as well.

Entity Spawn

Networking

Pragma uses a server/client model for networking. Entities can be either clientside, serverside or shared (=synchronized), depending on the entity type.

  • Some entities can only be created clientside (e.g. the viewmodel), some only serverside (e.g. the game_player_spawn entity), some on either side.
  • If a shared entity is created clientside, it will act as a clientside-only entity.
  • If a shared entity is created and spawned serverside, it will automatically create and spawn a clientside counterpart for all clients, and the entity will be synchronized from server to clients.

There are two methods that can be used to change an entity's networking behavior: Entity:SetShared, Entity:SetSynchronized.

local ent = ents.create("npc_zombie")
if(ent ~= nil) then
	-- Since npc_zombie is a shared entity, spawning it will also cause it to be created on the clients.
	-- It will then continuously be synchronized from server to client (e.g. position and rotation will be
	-- send from server to clients)
	ent:Spawn()
end

-------------------

local ent = ents.create("npc_zombie")
if(ent ~= nil) then
	-- Setting an entity to not be shared before it's spawned will make it effectively serverside only!
	-- The entity will not be created clientside, and it will not be synchronized.
	ent:SetShared(false)
	ent:Spawn()
end

-------------------

local ent = ents.create("npc_zombie")
if(ent ~= nil) then
	-- Setting an entity to not be shared AFTER it has been spawned will deactivate all network
	-- transmission for this entity from the server to the clients. The entity will still have a
	-- clientside representation, but the link between server and client will be cut.
	-- Note that this means that if the entity has been removed serverside, it will still linger
	-- clientside!
	ent:Spawn()
	ent:SetShared(false)
end

-------------------

local ent = ents.create("npc_zombie")
if(ent ~= nil) then
	-- You can also disable synchronization for this entity, which essentially means the entity
	-- will no longer be part of any snapshots. The entity will still be affected by events, which means
	-- that, for example, it will be removed clientside if the entity has been removed serverside.
	ent:Spawn()
	ent:SetSynchronized(false)
end

Creating a Lua-scripted entity

To create a new Lua-scripted entity, create a new Lua-file in one of "lua/entities", "lua/npcs", "lua/weapons" or "lua/vehicles", depending on the type of entity you wish to create. All entity types are inherently the same, so there is no functional difference between the different types, the different directories are merely for better organization. The name of the file should be the entity's class name, which should be lower case, only contain standard ASCII characters, and shouldn't contain any spaces. It should preferably also have the type of the entity as a prefix, here are some examples:

  • env_light: An environment entity
  • func_water: A functional brush entity
  • game_player_spawn: A game/gamemode entity
  • logic_relay: A logical entity
  • npc_zombie: A NPC
  • point_target: A point-based entity
  • prop_physics: A prop entity
  • trigger_gravity: A trigger entity
  • vhc_truck: A vehicle entity
  • weapon_smg: A weapon entity

The Lua-file should then contain the following line:

ents.register("classname",{"component1","component2","component3"},Entity.TYPE_SHARED)

The classname should be exactly the same as the file name (without the "lua" extension). What follows is a list of components that the entity should be created with. If the entity should be serverside/clientside only, move it to the "server"/"client" sub-directory (e.g. "lua/entities/server"), otherwise it will be automatically defined as a shared entity.

Note: The Lua-files for all clientside and shared entities/components will automatically be transferred to the clients. Also, entities (and components) are not automatically loaded at game start, they will be loaded when actually needed.

Usually, when creating a new custom entity, you'll want to create a new component as well. This is where the actual logic will be handled.

Creating a Lua-scripted component

Create a new directory in one of "lua/entities/components", "lua/weapons/components", "lua/vehicles/components" or "lua/npcs/components", depending on where your component fits best. The naming conventions are the same as for entities, except that components usually don't have a type prefix. If you want to create a component that can be used serverside, you'll also have to create a new sub-directory called "server". Inside it, create a Lua-file called "init.lua". Respectively, if you want to create a component that can be used clientside, you'll need a "client" sub-directory. In this case the Lua-file should be called "cl_init.lua". For most components, you'll generally want both.

All serverside Lua-files should go into the "server" directory, all clientside Lua-files into "client", and all shared Lua-files into the root component directory. If your component is either serverside only or clientside only, you don't need to create any sub-directories and can just put all Lua-files into the root component directory. All shared and clientside Lua-files will automatically be transferred to the clients, unless the component is serverside only.

For this tutorial we'll create a shared component, which means you should create a "shared.lua" inside the root component directory. This is not required, but highly recommended. Since the shared.lua will not be loaded automatically, you will have to include it in both the init.lua and cl_init.lua:

include("../shared.lua")

Now add the following, minimal required code to the shared.lua:

util.register_class("ents.CustomEntityComponent",BaseEntityComponent)

function ents.CustomEntityComponent:Initialize()
end
ents.COMPONENT_CUSTOM = ents.register_component("custom_component",ents.CustomEntityComponent,ents.EntityComponent.FREGISTER_BIT_NETWORKED)

Detailed explanation:

util.register_class("ents.CustomEntityComponent",BaseEntityComponent)

This will create a new Lua-class CustomEntityComponent inside the ents-library. For components, this class always has to be derived (directly or indirectly) from the base class BaseEntityComponent, which is specified in the second parameter. Replace CustomEntityComponent with your own name, but make sure to choose a name which is unlikely to cause naming conflicts with other addons.

function ents.CustomEntityComponent:Initialize()
end

This defines the method Initialize as part of the new Lua class. Initialize is one of the reserved method names for functions which will automatically be called by the engine. Here's a list of all of the reserved function names and when they are called:

  • Initialize: Called immediately after the component has been created. Use this function to initialize member variables, etc.
  • OnRemove: Called just before the component gets removed. Use this function for cleanup if necessary.
  • OnEntitySpawn: Called when the entity this component is attached to is spawned. Note that if the component is added to an entity after the entity was spawned, this method will get called immediately.
  • OnAttachedToEntity: Called when this component is added to an entity. Use this function to initialize code which is dependent on the entity.
  • OnDetachedFromEntity: Called when this component is removed from an entity. Note that this function is called just before the component is actually detached, so GetEntity will still return a valid reference to the entity. Since components cannot exit without an entity, this function being called means the component will be added to another entity immediately after.
  • Save: Called when a savegame is being created. Use this function to save data required to restore this component's current state.
  • Load: Called when a savegame is loaded. Use this function to restore the component's state.
  • SendData (serverside only): This will be called after the component has been initialized and the entity is being spawned/has been spawned. Only called if this is a networked component. Use this function to send data required to initialize the component clientside.
  • SendSnapshotData (serverside only): Use this function to send data for this component every snapshot. Note that this function has to be enabled both serverside and clientside using EntityComponent.SetShouldTransmitSnapshotData first. Sending snapshot data is expensive and should be avoided if possible.
  • ReceiveData (clientside only): Called clientside after the component's entity has been spawned serverside. Only called if this is a networked component. Use this function to receive data which was sent by the server using SendData.
  • ReceiveSnapshotData (clientside only): Called clientside every snapshot, if BaseEntityComponent.SetShouldTransmitSnapshotData has been enabled. Use this function to receive data which was sent by the server using SendSnapshotData.
  • ReceiveNetEvent: Called serverside if the client-side component has send a net-event, and the other way around.

In general it is a good idea to always define at least Initialize.

ents.COMPONENT_CUSTOM = ents.register_component("custom_component",ents.CustomEntityComponent,ents.EntityComponent.FREGISTER_BIT_NETWORKED)

So far the component was just a Lua-class, this function is required to register it as an actual component. Choose your own names for ents.COMPONENT_CUSTOM and custom_component. ents.COMPONENT_CUSTOM will be a unique id, which can be used to identity the component. Alternatively the string name "custom_component" can also be used to identify it, but in general you should be using the id.

If the component should be shared, and will transfer data between server and client, use ents.EntityComponent.FREGISTER_BIT_NETWORKED as third argument. Otherwise you can use ents.EntityComponent.FREGISTER_NONE.

In a lot of cases your component depends on other components, or needs to communicate with other components. To make this easier, you can use the helper functions 'AddEntityComponent and BindEvent:

function ents.CustomEntityComponent:Initialize()
	self:AddEntityComponent(ents.COMPONENT_TRANSFORM) -- Adds a transform component to the entity to allow changing its position and orientation.
	self:AddEntityComponent(ents.COMPONENT_HEALTH,"InitializeHealth") -- Adds a health component to the entity. "InitializeHealth" will be called with the health component as parameter, to allow you to initialize code that depends on that component.

	self:BindEvent(ents.HealthComponent.EVENT_ON_HEALTH_CHANGED,"OnHealthChanged") -- Binds the "OnHealthChanged" function to an event of the specified component. The function will be called whenever that event was triggered (in this case whenever the entity's health has changed).
end
function ents.CustomEntityComponent:InitializeTransform(component)
	component:SetPos(Vector(1,2,3))
end
function ents.CustomEntityComponent:InitializeHealth(component)
	component:SetMaxHealth(200)
	component:SetHealth(100)
end
function ents.CustomEntityComponent:OnHealthChanged(oldHealth,newHealth)
	print("Health has changed from ",oldHealth," to ",newHealth)
end

Note that AddEntityComponent will not add the component if the entity already has a component of that type, however the specified function will still get called.

Events

Components generally communicate via events. For example, the health component will fire the EVENT_ON_HEALTH_CHANGED event whenever its health has changed, and any other component of the same entity can listen to this event and implement a reaction. To listen to an event, you can use the BindEvent function as shown above.

You can also use AddEventCallback if you want to listen to an event outside of a component:

local pl = ents.get_local_player()
local healthComponent = (pl ~= nil) and pl:GetEntity():GetComponent(ents.COMPONENT_HEALTH) or nil
if(healthComponent ~= nil) then
	local callback = healthComponent:AddEventCallback(ents.HealthComponent.EVENT_ON_HEALTH_CHANGED,function(oldHealth,newHealth)
		-- Do something
	end)
end

If you want to broadcast your own event, you'll have to register it first:

ents.CustomEntityComponent.EVENT_ON_CUSTOM = ents.register_component_event("ON_CUSTOM")

Now you can broadcast it from within your component using BroadcastEvent:

function ents.CustomEntityComponent:Initialize()
	self:BroadcastEvent(ents.CustomEntityComponent.EVENT_ON_CUSTOM,{"argument1",Vector(1,2,3),5.0}) -- Broadcast an event with arguments
end

Networking

Networking your component between server and client is only possible if you have registered it with the ents.EntityComponent.FREGISTER_BIT_NETWORKED flag. There are four types of networking for components:

Send/ReceiveData

If you need to transmit data from server to client before initializing the component clientside, you can use the SendData and ReceiveData functions:

-- Serverside
function ents.CustomEntityComponent:SendData(packet,recipientFilter)
	packet:WriteFloat(5.0)
	packet:WriteVector(Vector(1,2,3))
end

-- Clientside
function ents.CustomEntityComponent:ReceiveData(packet)
	local f = packet:ReadFloat()
	local v = packet:ReadVector()
end

Send/ReceiveNetEvent

-- Shared
ents.CustomEntityComponent.NET_EVENT_CUSTOM = net.register_event("custom")

-- Serverside
function ents.CustomEntityComponent:OnHealthChanged(oldHealth,newHealth)
	local packet = net.Packet()
	packet:WriteFloat(oldHealth)
	packet:WriteFloat(newHealth)
	self:BroadcastNetEvent(net.PROTOCOL_TCP,ents.CustomEntityComponent.NET_EVENT_CUSTOM,packet)
end

-- Clientside
function ents.CustomEntityComponent:Initialize()
	self:BindNetEvent(ents.CustomEntityComponent.NET_EVENT_CUSTOM,"OnReceivedCustom")
end

function ents.CustomEntityComponent:OnReceivedCustom(packet)
	local oldHealth = packet:ReadFloat()
	local newHealth = packet:ReadFloat()
end

Send/ReceiveSnapshotData

-- Shared
function ents.CustomEntityComponent:Initialize()
	self:SetShouldTransmitSnapshotData(true)
end

-- Serverside
function ents.CustomEntityComponent:SendSnapshotData(packet,pl)
	packet:WriteFloat(5.0)
	packet:WriteVector(Vector(1,2,3))
end

-- Clientside
function ents.CustomEntityComponent:ReceiveSnapshotData(packet)
	local f = packet:ReadFloat()
	local v = packet:ReadVector()
end

Networked Variables

-- Serverside
function ents.CustomEntityComponent:Initialize()
	self:SetNetworkedFloat("nw_test",5.0)
end

-- Clientside
function ents.CustomEntityComponent:SomeFunction()
	local f = self:GetNetworkedFloat("nw_test")
end