Entity-Component-System Conventions

Prototypes

Prototype data-fields

Don’t cache prototypes, use prototypeManager to index them when they are needed. You can store them by their ID. When using data-fields that involve prototype ID strings, use ProtoId. For example, a data-field for a list of prototype IDs should use something like:

[DataField]
public List<ProtoId<ExamplePrototype>> ExampleTypes = new();

EntityUid in Logs

When using EntityUid in admin logs, use the IEntityManager.ToPrettyString(EntityUid) method.

Admin log with entities example (click to expand)
// If you're in an entity system...
_adminLogs.Add(LogType.MyLog, LogImpact.Medium, $"{ToPrettyString(uid)} did something!");

// If you're not in an entity system...
_adminLogs.Add(LogType.MyLog, LogImpact.Medium, $"{entityManager.ToPrettyString(uid)} did something!");

Optional Entities

If you need to pass “optional” entities around, you should use a nullable EntityUid for this. Never use EntityUid.Invalid to denote the absence of EntityUid, always use null and nullability so we have compile-time checks. e.g. EntityUid? uid

Components

Component data access modifiers

All data in components should be public.

Component property setters

You may not have setters with any logic whatsoever in properties. Instead, you should create a setter method in your entity system, and apply the [Friend(...)] attribute to the component so only that system can modify it. Your component may use properties with setter logic for ViewVariables integration (until we have a better system for that).

Component access restrictions

The [Access(...)] attribute allows you to specify which types can read or modify data in your class, while prohibiting every other type from modifying it.

Components should specify their access restrictions whenever possible, usually only allowing the entity systems that wrap them to modify their data.

Shared Component inheritance

If a shared component is inherited by server and client-side counterparts, it should be marked as abstract.

Entity Systems

Game logic

Game logic should always go in entity systems, not components. Components should only hold data.

Proxy Methods

When possible, try using the EntitySystem proxy methods instead of using the EntityManager property.

Examples (click to expand)
// Without proxy methods...
EntityManager.GetComponent<MetaDataComponent>(uid).EntityName;

// With proxy methods
Name(uid);

// Without proxy methods...
EntityManager.GetComponent<TransformComponent>(uid).Coordinates;

// With proxy methods
Transform(uid).Coordinates;

Events

Method Events vs Entity System Methods

Method Events are events that you raise when you want to perform a certain action. Example:

// This would change the damage on the entity by 10.
RaiseLocalEvent(uid, new ChangeDamageEvent(10));

On the other hand, Entity System Methods are methods you call on systems to perform an action.

// This would change the damage on the entity by 10.
EntitySystem.Get<DamageableSystem>().ChangeDamage(uid, 10);

Method Events are prohibited, always use Entity System Methods instead. There’s an exception to this, however.

You may use Method Events as long as they’re wrapped by an Entity System Method. In the example above, this would mean that DamageableSystem.ChangeDamage() would internally raise the ChangeDamageEvent, which would then by handled by any subscriptors…

Info

Ensure events are unsubscribed from when systems are shutdown. Proxy methods like Subs.CVar()or SubscribeLocalEvent already take care of it, note that you do not need to unsubscribed inside managers, as their lifetime ensures that when they shutdown, the rest of the client / server is also shutting down, making unsubscribing not necessary.

Event naming

  • Always suffix your events with Event. Example: DamagedEvent, AnchorAttemptEvent

  • Always name your event handler like this: OnXEvent Example: OnDamagedEvent, OnAnchorAttemptEvent

Struct by-ref events

Events should always be structs, not classes, and should always be raised by ref. If possible it should also be readonly if applicable. They should also have the [ByRefEvent] attribute.

In practice this will look like the following:

var ev = new MyEvent();
RaiseLocalEvent(ref ev);

Subpages