Skip to content

Extending the Controller

Heep edited this page Feb 26, 2018 · 4 revisions

The Controller is a bare-bones component which makes all the magic happen, but is useless by itself. All the movement code is provided by MovementController, which derives from the Controller. There are several virtual methods provided that can be overridden and extended with ease, which should allow to implement any functionality needed.

There are also modules, which receive callbacks from the controller after each tick. They are good for visual things like animations, but should not be relied on for actions that need accurate data like weapon shooting.

Deriving from the Controller

The following approach uses virtual function overriding. It is possible to do without any overriding by manually adding in functions, giving extra performance, but it is less clean than function overriding. At the moment there are 10 functions that could be overridden, each of them is listed inside of the abstract class BaseController. Here is the description of most of them:

OnStart function is equivalent of Unity's Start function. This is one of the functions that if it is overridden, it is crucial to call the base function, since otherwise controller's data will not be properly setup and a lot of issues will occur.

OnBeingDestroyed is called from OnDestroy, it is a place for all cleanup operations. Be sure to call the base function or unregister the controller from the game manager manually.

IsCommandValid checks the user's inputs on the server side for possible external manipulation. Return value of 1 means everything is okay, return value of 0 means something is wrong, but that might be an accident and the return value of -1 means that there has been some manipulation of data. On the last case, you are free to consider it as cheating and handle the event accordingly.

InputUpdate is called from the Update loop in order to update the command for any input occurances. The current controller fills in movement axis, look direction, jump and crouch keys. The keys are stored inside a bitfield and there are up to 32 keys available.

RunCommand is the function that processes all the input and returns the result. It is not implemented inside the Controller class, but rather is inside MovementController. By the idea, the code should back up GameManager.curtime, set it to inputs.timestamp * sendrate, call RunPreMove, run the movement code, call RunPostMove and restore the curtime. Check MovementController's implementation for the specifics.

RunPreMove/RunPostMove are the functions that by design should be called before and after the movement code. The original functions are not required to be called if deriving from Controller or MovementController since they implement empty implementations.

ProcessInterpolation is in place for anything to be interpolated during rendering. While ProcessTeleportaion is called instead if the player moved too much in one tick (could be anything like SetPosition call or big lag).

The example of the application of function overriding is provided inside ExampleCharacter script, implementing AI targets and shooting with time delay.

PredVar

PredVar is a custom data storage type meant for data synchronization and ability to predict. Use this data structure for everything predictable that occurs inside RunCommand and the functions called from it. It is important to note that UNet SyncVars will not work on classes deriving from the Controller since it overrides OnSerialize and OnDeserialize. PredVar is a generic type but network read and write functions need to be implemented for each type. One downside is that PredVar is a class. This is needed for implementing the read and write functions. If it is possible, copying and pasting the whole generic class and defining it as a struct would bring a lot of performance benefits if accessing the data frequently. Otherwise, the data will be allocated in the heap, but the implementation of the type will be easier and will look like this:

[System.Serializable]
public class PredVar_uint : PredVar<uint>
{
	public PredVar_uint(uint val) : base(val) { }

	protected override uint ReadVal(NetworkReader reader) {
		return reader.ReadPackedUInt32();
	}

	protected override void WriteVal(NetworkWriter writer, uint value) {
		writer.WritePackedUInt32(value);
	}
}

At the moment NetworkReader and NetworkWriter is used for data writing, later on a custom solution might be made making the generic PredVar work without the need of per-type implementations.

Providing more user input

While it is not possible to pass more floating point or integer data inside the Inputs structure, there is a keys bitfield, allowing set each bit of the field on and off to tell the server if a specific key is pressed or not. A helper structure CFlags was made to provide with easier ways to deal with the exceptionally long and rather complicated bit testing inside C#. Firstly, a mask is needed. For a single key, the mask would be defined as (1u << n), where n is index from 0 to 31. After that you are free to use the CFlags helper methods IsSet, Set or C++ like (and C#, but shortened) bit testing using & and | operators.

Modules

Modules are separate scripts that are added to the Controller's GameObject and receive callbacks from the Controller once a tick occurs. They are useful for things like animation handling and effects. Modules have to get the Controller component manually and register to the respective delegate. Controller provides these 3 delegates: tickUpdateNotify, tickUpdate, tickUpdateDebug. tickUpdateNotify accepts just one argument, bool inLagCompensation and it is meant for notifying the script that an update occur. tickUpdate has an additional argument before inLagCompensation, which is the Results struct. tickUpdateDebug is meant for debugging purposes and receives the third Inputs struct argument, but it is not guaranteed to stay there.

Registering on the controller is done this way:

public Controller controller;
private bool added;

void Start () {
	if (!added) {
		controller.tickUpdate += this.TickUpdate;
		added = true;
	}
}

void OnEnable () {
	if (!added) {
		controller.tickUpdate += this.TickUpdate;
		added = true;
	}
}

void OnDisable () {
	if (added) {
		controller.tickUpdate -= this.TickUpdate;
		added = false;
	}
}

public void TickUpdate (Results res, bool inLagCompensation) {
	//Handle the update, set animation data, spawn effect particles etc.
}