I've completed the engine redesign. Here's a list of what's changed:
- The engine is now designed to use a shared codebase for client and server. Instead of separate types and assemblies everything is together to make it easier to write and use code that needs to be used on both client and server.
- Model formats are now mostly encapsulated by a single interface called
IModelFormatProvider
. This interface allows a loader and renderer to be provided for a particular format. This design will need some reworking later on to support renderers that don't use a specific format (i.e. beams).
- Maps are now represented in-engine as a scene, divided into the entities and graphical parts.
- Model loading has been greatly simplified. Loading is now done by using the ModelManager instance for a given scene, and graphics resources are loaded and uploaded to the GPU as needed. Models loaded at runtime load just fine. For studio models the error model will be used, but support for the error model for brush and sprite models does not currently work. The concept of a model index has been completely removed, as is the concept of precaching models.
- World state has been centralized in a
WorldState
class to make access easier. The renderer can be accessed from here as well, and for server code a dummy renderer will exist to make writing code easier.
- The old inheritance based entity system has been replaced with a component based system. Entities are now comprised out of zero or more components. Functionality like position, angles and velocity is now contained in the
Transform
component, models are subclasses of the RenderableComponent
component, physics is handled by the Collider
component.
Specific functionality is being reworked as discrete components to reduce complexity and make code reusable. For example,
func_door
's movement logic (inherited from
CBaseToggle
in the original SDK) is now its own component called
LinearMovementLocomotor
. Code specific to
func_door
(automatic return, touch, etc) is contained in the
LinearDoor
component.
- For backwards compatibility components can specify the original keyvalue name to use for a public member, as well as a custom converter should that be necessary. Converters handle conversion between string and the keyvalue type, with support for common types provided already.
In similar fashion spawnflags can be automatically converted to booleans. The
SpawnFlag
attribute allows you to specify which bit maps to a boolean.
- Game code is now reduced to just actual game logic. The renderer as well as physics code has been moved into the engine. It is expected that modders modify the engine itself as needed to suit their needs, although the game API should allow for most basic tasks.
- The game assembly is loaded dynamically as before in order to make it easier to work with plugins. The game assembly is listed in the engine configuration file as a plugin, and treated as such. This means plugins will be able to do everything that game code can do. Due to how these assemblies are loaded, plugins can override entity factories defined in the game assembly, allowing you to replace entities.
Since the component based design removes the use of specific entity classes this is no longer a serious problem and potential crash issue, but game code that works with classnames (should be nothing beyond logging) might not work as expected.
Plugins can be server, client or both client and server, although i don't plan to allow downloading of plugins. I do plan to support the use of CSharpScript scripts loaded at map start time, which can do server-only tasks like defining non-networked components, new entity factories and replacing existing factories. At present, plugins are loaded at startup time only, to allow all data related to them to be made immutable.
- The event system has been redesigned to simplify it and make it easier to use. It is no longer necessary to define events ahead of time, it is no longer required to inherit from the
EventData
class when defining custom event data, and all system operations will work even when in an event dispatch. As such the workaround for this, post dispatch callbacks, have been removed. To avoid state corruption any operations executed while in an event dispatch are queued up for execution after the dispatch has completed.
The event system is more memory and CPU efficient as a result.
- The object editor has been updated to support editing component based entities.
Some examples of the new stuff:
Defining a component with keyvalue name changes, spawnflags:
/// <summary>
/// A door that uses a <see cref="LinearMovementLocomotor"/> to move
/// </summary>
public sealed class LinearDoor : Component
{
private LinearMovementLocomotor _locomotor;
private ToggleState _toggleState = ToggleState.AtBottom;
private Collider Activator;
public Vector3 Position1;
public Vector3 Position2;
[KeyValue(Name = "wait")]
public float Wait;
[KeyValue(Name = "lip")]
public float Lip;
[SpawnFlag(1 << 0)]
public bool StartsOpen;
[SpawnFlag(1 << 3)]
public bool Passable;
[SpawnFlag(1 << 5)]
public bool NoAutoReturn;
[SpawnFlag(1 << 8)]
public bool UseOnly;
[SpawnFlag(1U << 31)]
public bool Silent;
}
A basic light entity factory:
/// <summary>
/// Non-displayed light.
/// Default light value is 300
/// Default style is 0
/// If targeted, it will toggle between on or off.
/// </summary>
[LinkEntityToFactory(ClassName = "light")]
[LinkEntityToFactory(ClassName = "light_spot")]
public class LightFactory : EntityFactory
{
protected override void GetComponentTypes(ImmutableHashSet<Type>.Builder types)
{
types.Add(typeof(Transform));
types.Add(typeof(Light));
}
public override bool Initialize(EntityCreator creator, Entity entity, IReadOnlyList<KeyValuePair<string, string>> keyValues)
{
if (!creator.InitializeComponent(entity.GetComponent<Transform>(), keyValues))
{
return false;
}
if (!creator.InitializeComponent(entity.GetComponent<Light>(), keyValues))
{
return false;
}
return true;
}
}
Defining a keyvalue converter for ints:
[KeyValueConverter(typeof(int))]
public sealed class IntConverter : IKeyValueConverter
{
public object FromString(Type destinationType, string key, string value) => KeyValueUtils.ParseInt(value);
}
The next task is once again to get player physics working so interaction in the world is possible.