6: Events, Templates, and Interfaces
Last Updated: 10/8/2020Note: This is NOT engine documentation. I write these articles as I learn. As such, information present here may be incorrect or out-of-date. I'm leaving these pages online for historical purposes only.
Up to this point I was hard-coding listeners for PhysX events to test with, but I needed a more robust solution. I needed any object to be able to subscribe to certain types of events, track their senders, and automatically manage cleanup when one or the other is deallocated. Enter Interfaces. Since C++ does not have interfaces as a concept, I implemented them as classes and leveraged multi-inheritance. However, because of the way multi-inheritance works in C++, interfaces cannot descend from a base class to avoid the Diamond Inheritance Problem.
IPhysicsActor
This interface allows an object to receive physics object collision events. The client programmer simply inherits from this interface, and then registers the object to receive events from any PhysicsBodyComponent. The receiver does not need to be the same object as the sender, and if the receiver is deallocated before the sender, the receiver removes itself from the sender's list on destruction. Event listeners follow a weak ownership model, they do not have strong pointers to their senders. An example of this is shown below://the controller script
class TestEntityController : public RavEngine::ScriptComponent, public RavEngine::IPhysicsActor {
public:
//override the collision entered event
void OnColliderEnter(const WeakRef& c) override{
std::cout << "Hit by collider @ " << &c.get()->getOwner() << std::endl;
}
};
//the entity itself
class TestEntity : public RavEngine::Entity {
protected:
static Ref sharedMat; //all the objects can share a single instance of the physics material
public:
TestEntity() : Entity(){
//attach the script
auto script = AddComponent(new TestEntityController());
//create a dynamic rigid body
auto r = AddComponent(new RigidBodyDynamicComponent(FilterLayers::L0,FilterLayers::L0 | FilterLayers::L1));
//set the script to be an events receiver for the rigid body
r->AddReceiver(script.get());
//add a box collision to the PhysX component
if (sharedMat.isNull()) {
//note: if constructing on multiple threads, this static must be locked or made atomic
sharedMat = new PhysicsMaterial(0.5, 0.5, 0.5);
}
AddComponent(new BoxCollider(vector3(1, 1, 1),sharedMat));
}
};
Because the class is not abstract, only the events that the client programmer cares about need to be overridden.
IInputListener
This interface allows any object to receive input events from a connected controller, like a keyboard or gamepad. This one works differently from the physics event listener. Unlike the physics system, which has a fixed set of possible events, there is no way to predict ahead of time how many input events a given object may receive. Additionally, I did not want to require programmers to use a giant if-statement or switch to check which event is received through a single entry method. IInputListener serves mainly to track senders and mark the object as receivable.The magic happens with templates. The InputManager class contains templated methods to bind and unbind an event to any method the programmer wants on their object as long as it meets the signature requirement. It does this using nested classes which contain std::functions that are bound from plain C function pointers.
template<class U>
void BindAction(const std::string& name, IInputListener* thisptr, void(U::* f)(), ActionState type, CID controllers);
template<typename U>
void BindAxis(const std::string& name, IInputListener* thisptr, void(U::* f)(float), CID controllers, float deadZone = AxisCallback::defaultDeadzone);
Note the second and third parameters. The second parameter requires the object be an IInputListener so that it correctly cleans up when it is deallocated,
and the third parameter allows proper scoping with the function pointer. With this setup, a programmer can easily bind functions to their classes with
clean syntax, UE4 style:
// PlayerScript.hpp
class PlayerScript : public RavEngine::ScriptComponent, public RavEngine::IInputListener {
public:
void MoveForward(float amt){
printf("Forward axis activated! Amount = %f\n",amt);
}
void Jump(){
printf("Jump Action was Pressed!\n")
}
};
//GameWorld.hpp
Ref<RavEngine::InputManager> is = new RavEngine::InputManager();
//setup control mappings
is->AddAxisMap("MoveForward", SDL_SCANCODE_W);
is->AddAxisMap("MoveForward", SDL_SCANCODE_S, -1); //go backwards
is->AddAxisMap("MoveForward", ControllerAxis::SDL_CONTROLLER_AXIS_LEFTY, -1); //gamepad
is->AddActionMap("Jump", SDL_SCANCODE_SPACE);
is->AddActionMap("SampleFPS", ControllerButton::SDL_CONTROLLER_BUTTON_Y); //gamepad
//bind the actions
auto con = CID::C0; //only bind to the first controller
auto playerscript = player->Components().GetComponent<PlayerScript>().get();
//since the bind calls are templated, you can pass the address of the method on the class without issues
is->BindAxis("MoveForward", playerscript, &PlayerScript::MoveForward,con);
is->BindAxis("MoveRight", playerscript, &PlayerScript::Jump,con);
You can even get input events directly in the World, which is very useful for debugging or having global events like pausing.
Telling controllers apart
Notice in the above code sample the variablecon
. The final parameter in the bind methods requires the programmer to pass which
controller the events should respond to. I implemented controller binding in such a way that a binding can listen to any number of different controllers,
or all of them at once, with control. I implemented this using bitmasks. Each controller is registered internally simply with a number, with the keyboard
and mouse hardcoded to index 0, and gamepads using any id above that. When the engine gets an input event, it creates a CID bitmask on it, and checks if
any bits match between the mapping and the input. If so, the event is triggered. Since I use a 32 bit int as the bit mask, in theory RavEngine can support
up to 32 controllers. I currently limit it to 16 but I can't even test that many since I only have my keyboard and a single Xbox One controller. Shown below
is the implementation of the CID enum:
//controller IDs
enum class CID{
NONE = 0,
C0 = 1 << 0, //reserved for the Keyboard and Mouse
C1 = 1 << 1,
C2 = 1 << 2,
C3 = 1 << 3,
C4 = 1 << 4,
// ...
ANY = ~0 //all bits set, so any controller will trigger
};
//bitwise operators for enum class
inline CID operator | (CID lhs, CID rhs)
{
using T = std::underlying_type_t<CID>;
return static_cast<CID>(static_cast<T>(lhs) | static_cast<T>(rhs));
}
inline CID& operator |= (CID& lhs, CID rhs)
{
lhs = lhs | rhs;
return lhs;
}
inline CID operator & (CID lhs, CID rhs)
{
using T = std::underlying_type_t<CID>;
return static_cast<CID>(static_cast<T>(lhs) & static_cast<T>(rhs));
}
inline CID& operator &= (CID& lhs, CID rhs)
{
lhs = lhs & rhs;
return lhs;
}
A side note: I noticed that Xbox controllers have a very high amount of stick drift. I am not pressing anything in the video below:
Xbox analog sticks need a deadzone of around 10% in order to eliminate the drift, which I think is quite high.
Next up: ECS Megapost