- An Unreal Engine project running on 4.22
- It is an example of how to implement the inversion of control (IOC) pattern using Unreal Engine's
TTypeContainer
You might want to skip to the "How does it work" section as it's more of an overview, rather than an in detail look at everything.
Class | Description | Note |
---|---|---|
Common.h | A header that should be included in all files concerned with access to the IOC container | You'll notice it is at the top of most, if not all, of my files. Here is an example. |
UIOC |
Provides global access to the TTypeContainer |
It's a UCLASS to help with memory management and to allow for further development |
UMainGameInstance |
It's job in this project is to register interfaces to their implementations for gameplay | The constructor runs a private method called SetUpIOCRegistrations |
APlayerPawn |
Provides an example for how we resolve instances from the IOC container | Check out the constructor which uses the globally accessible UIOC static Container property to resolve a PlayerService by the IPlayerService interface. Also, check out the TriggerVolumeHitOccured delegate method that uses the service to demonstrate how our tests work |
AEnemyPawn |
Works with the APlayerPawn to demonstrate how a test works | Check out the TakeHit method for what our test is actually concerned about |
IPlayerService |
The interface which we register PlayerService and other implementing implementations to |
Check out the Expose_TNameOf(IPlayerService) which is required by the RegisterClass which is on the TTypeContainer . It is used to create a simple struct that allows the internals of TTypeContainer to reference your interface by name. Check it out in the UnrealTypeTraits.h file |
PlayerService |
Is an implementation of the IPlayerService . It's not particularly interesting in terms of what it does, but it works as an example for our testing |
Check out the UMainGameInstance 's SetUpIOCRegistrations method's registration of interface to implementation. |
PlayerPawnTests | Runs a few tests against our above code | Check out the instantiation of PlayerPawnTestsAgent inside its belly. I found it useful to organise my tests in this way |
PlayerPawnTestsAgent |
Simply a way of managing a set of tests. I thought it was a nice way of laying them out. I'm sure there are much better ways :). I mean, I did try, just look at this messy commit | Check out the constructor where i instantiate a test specific MockPlayerService that helps me with my testing. Also notice that I call UIOC::Container.RegisterInstance... which sets up my container specifically for this test. My APawn 's PlayerService instance will (for my test) now be my MockPlayerService . Pretty cool right! |
- The
UIOC
is declared in a Common.h file that is included in all files that need access to the IOC container. - The
UIOC
exposes a staticTTypeContainer
that is Unreal Engine's helper class for registering interfaces (IPlayerService
) to implementations (PlayerService
,MockPlayerService
). - The
UIOC
Container is populated within theUMainGameInstance
constructor:
UIOC::Container.RegisterClass<IPlayerService, PlayerService>();
- Because the
UIOC
is globally accessible and has a staticContainer
property, we access this from within ourAPlayerPawn
- Inside
APlayerPawn
's constructor, this is run:
// Fetch PlayerService from IOC container
PlayerService = UIOC::Container.GetInstance<IPlayerService>();
- Then inside the actual
APlayerPawn
methodTriggerVolumeHitOccured
, we can call thePlayerService
property which will use the implementation which we registered in theUMainGameInstance
constructor and resolved in the pawn's constructor.
// Get the power modifier for player
int powerModifier = PlayerService->GetPowerModifier(this);
// For example purposes, does a simple bit of logic to demonstrate test
// Note: Should be in service, but for brevity:
if (powerModifier >= EnemyPawn->Power)
{
// Issue damage to enemy
EnemyPawn->TakeHit();
}
- The test is quite simple and is declared inside the PlayerPawnTests.cpp:
IMPLEMENT_SIMPLE_AUTOMATION_TEST(PlayerPawnTests, "Pawns.PlayerPawnTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
- It instantiates a little wrapper class
PlayerPawnTestsAgent
where in the constructor we do something really interesting! If you were to debug this method, you will notice that even though the Container was registered to inside theUMainGameInstance
, it is now empty. This is because our Game code is not running the constructor of theUMainGameInstance
. Phew, well that's helpful, because now we can register our test specific versions of theIPlayerService
:
// Create an instance of the mock player service
TSharedRef<IPlayerService> SharedMockPlayerService = MakeShareable(new MockPlayerService());
// Register the instance to our container
UIOC::Container.RegisterInstance<IPlayerService>(SharedMockPlayerService);
- Then we go ahead and run our tests, and if you debug while doing so, you'll notice that the instance inside the
APlayerPawn
is ourMockPlayerService
instead of thePlayerService
.
- IOC (Inversion of control) is helpful for testing
- It removes the dependency of an implementation, and instead concerns itself only with the interface which exposes all the operations, without any of the annoying implementation details.
- The above is important because now we can put together specific data to test against. It's easy to know that (See test example) 1 - 1 is 0. But not so easy to know that (for example) x - 1 = 0. Note the enforced value being returned from the method:
// Inherited via IPlayerService
virtual int GetPowerModifier(APlayerPawn * PlayerPawn) override
{
return 1;
}
- Unreal Engine actually has documentation of this (mentioned in the TTypeContainer write up) under a cpp file. It can be found here.
author: jimmyt1988 23/05/2019