-
Notifications
You must be signed in to change notification settings - Fork 1
Home
This page will help you get familiar with the SingleplayerDemo codebase and the Strife.Engine services.
The whole library is designed to be type safe. Thus, most of the code uses templates to generate efficient code and prevent type mismatches. It's multi-threaded and avoids memory allocation in as many places as possible. Here's the getting started guide:
Make an Input and Output object for the network (see GameML.hpp)
struct PlayerModelInput : StrifeML::ISerializable
{
void Serialize(StrifeML::ObjectSerializer& serializer) override
{
serializer
.Add(velocity)
.Add(grid);
}
Vector2 velocity;
GridSensorOutput<40, 40> grid;
};
enum class PlayerAction
{
Nothing,
Up,
Down,
Left,
Right
};
struct PlayerDecision : StrifeML::ISerializable
{
void Serialize(StrifeML::ObjectSerializer& serializer) override
{
serializer
.Add(velocity)
.Add(action);
}
Vector2 velocity;
PlayerAction action;
};
These must implement ISerializable. They automatically get compressed into a smaller format for storage in the sample repository. These can have any arithmetic type (float, int, etc), enums, Vector2, and GridSensorOutput. You can add other custom types by specializing the Serialize<> template.
Once you have your input and output, inherit from StrifeML::NeuralNetwork, which takes the input type, output type, and sequence length (for lstm). E.g.
struct PlayerNetwork : StrifeML::NeuralNetwork<PlayerModelInput, PlayerDecision, 1>
{
void MakeDecision(gsl::span<const InputType> input, OutputType& output) override
{
}
void TrainBatch(Grid<const SampleType> input, StrifeML::TrainingBatchResult& outResult) override
{
}
};
Note that InputType is a type alias for PlayerModelInput. Likewise with OutputType. SampleType holds both an input and output. The input for TrainBatch is a grid with dimensions # of batches rows x sequence length.
struct PlayerDecider : StrifeML::Decider<PlayerNetwork>
{
};
The decider is smart enough to not kick off a new decision if the one in progress isn't done yet.
struct PlayerTrainer : StrifeML::Trainer<PlayerNetwork>
{
PlayerTrainer()
: Trainer<PlayerNetwork>(32, 1) // Batch size 32, run once per second
{
samples = sampleRepository.CreateSampleSet("player-samples");
samplesByActionType = samples
->CreateGroupedView<PlayerAction>()
->GroupBy([=](const SampleType& sample) { return sample.output.action; });
}
void ReceiveSample(const SampleType& sample) override
{
samples->AddSample(sample);
}
bool TrySelectSequenceSamples(gsl::span<SampleType> outSequence) override
{
return samplesByActionType->TryPickRandomSequence(outSequence);
}
StrifeML::SampleSet<SampleType>* samples;
StrifeML::GroupedSampleView<SampleType, PlayerAction>* samplesByActionType;
};
-
SampleRepository
allows you to create grouped views, which allow you to group samples by some value on the sample. For example, grouping samples by the player action. -
ReceiveSample()
allows you to control how samples get stored. -
TrySelectSequenceSamples()
allows you to control how samples for a batch entry get picked. In this instance, it callsTryPickRandomSequence()
, which picks a random value from the grouped view (e.g. MoveLeft), and then picks a random sample from that group. It will then pick the preceding samples in time that led up to that sample (just like how we did it in chaser). - You can set the batch size, training frequency, and the minimum number of samples that have to be available before training starts by settings appropriate values on the trainer. When the trainer is done training a batch, it pushes an updated network to the client.
Before the game starts up (see Game::OnGameStart() in main.cpp), create the networks you want to be available in the game.
auto playerDecider = neuralNetworkManager->CreateDecider<PlayerDecider>();
auto playerTrainer = neuralNetworkManager->CreateTrainer<PlayerTrainer>();
neuralNetworkManager->CreateNetwork("nn", playerDecider, playerTrainer);
Also, register the types of objects you want the sensors to be able to detect (this replaced observedObjectType on the entity):
SensorObjectDefinition sensorDefinition;
sensorDefinition.Add<PlayerEntity>(1).SetColor(Color::Red()).SetPriority(1);
sensorDefinition.Add<TilemapEntity>(2).SetColor(Color::Gray()).SetPriority(0);
neuralNetworkManager->SetSensorObjectDefinition(sensorDefinition);
When creating the player (see PlayerEntity::OnAdded()), add a grid sensor component:
auto gridSensor = AddComponent<GridSensorComponent<40, 40>>("grid", Vector2(16, 16));
gridSensor->render = true;
Note that the size 40x40 must match the GridSensorOutput size since it's part of the type.
auto nn = AddComponent<NeuralNetworkComponent<PlayerNetwork>>();
nn->SetNetwork("nn");
This is how game data actually gets collected and sent to the Trainer and Decider.
// Called when:
// * Collecting input to make a decision
// * Adding a training sample
nn->collectInput = [=](PlayerModelInput& input)
{
input.velocity = rigidBody->GetVelocity();
gridSensor->Read(input.grid);
};
// Called when the decider makes a decision
nn->receiveDecision = [=](PlayerDecision& decision)
{
};
// Collects what decision the player made
nn->collectDecision = [=](PlayerDecision& outDecision)
{
};
That's it. Hopefully this walk-through helped you get familiar with the codebase so that you can start to tinker and make your own games using machine learning.