The FFNN-based AI is very simple. It's no big magic against it. The AI has the following inputs (mainly raycast distances):
Code:
{$ifdef FewerRays}
CountSensorRays=9;
CountSensorOpponentRays=5;
{$else}
CountSensorWallRays=19;
CountSensorFloorRays=3;
CountSensorCeilRays=3;
CountSensorOpponentRays=36;
{$endif}
PGameShipSensors=^TGameShipSensors;
TGameShipSensors=record
WayPointAngle:single;
Speed:single;
{$ifdef FewerRays}
Rays:array[0..CountSensorRays-1] of single;
{$else}
WallRays:array[0..CountSensorWallRays-1] of single;
FloorRays:array[0..CountSensorFloorRays-1] of single;
CeilRays:array[0..CountSensorCeilRays-1] of single;
{$endif}
OpponentRays:array[0..CountSensorOpponentRays-1] of single;
end;
and the following output:
Code:
PGameShipControllerInput=^TGameShipControllerInput;
TGameShipControllerInput=record
Steering:single;
Pitch:single;
Accelerate:single;
AirBrakeLeft:single;
AirBrakeRight:single;
Nitro:single;
Fire:single;
end;
All input & output values are in the 0.0 .. 1.0 range, which'll converted to or from the -1.0 .. 1.0 range, depending of the input/output value kind.
Code:
procedure TGameShip.UpdateAI(DeltaTime:double);
var Counter,Index:longint;
AnalogControlLowPassFilterCoef:single;
begin
if TGame(Game).Started then begin
AnalogControlLowPassFilterCoef:=1.0-exp(-(DeltaTime*100.0));
FFNN.Inputs[0]:=Sensors.WayPointAngle;
FFNN.Inputs[1]:=Sensors.Speed;
Index:=2;
{$ifndef FewerRays}
for Counter:=0 to CountSensorWallRays-1 do begin
FFNN.Inputs[Index]:=Sensors.WallRays[Counter];
inc(Index);
end;
for Counter:=0 to CountSensorFloorRays-1 do begin
FFNN.Inputs[Index]:=Sensors.FloorRays[Counter];
inc(Index);
end;
for Counter:=0 to CountSensorCeilRays-1 do begin
FFNN.Inputs[Index]:=Sensors.CeilRays[Counter];
inc(Index);
end;
{$endif}
for Counter:=0 to CountSensorOpponentRays-1 do begin
FFNN.Inputs[Index]:=Sensors.OpponentRays[Counter];
inc(Index);
end;
FFNN.Calculate;
ControllerInput.Steering:=FloatLerp(ControllerInput.Steering,Min(Max((FFNN.Outputs[0]*2.0)-1.0,-1.0),1.0),AnalogControlLowPassFilterCoef);
ControllerInput.Accelerate:=FloatLerp(ControllerInput.Accelerate,Min(Max((FFNN.Outputs[1]*2.0)-1.0,-1.0),1.0),AnalogControlLowPassFilterCoef);
ControllerInput.Pitch:=FloatLerp(ControllerInput.Pitch,Min(Max((FFNN.Outputs[2]*2.0)-1.0,-1.0),1.0),AnalogControlLowPassFilterCoef);
ControllerInput.AirBrakeLeft:=Min(Max(FFNN.Outputs[3],0.0),1.0);
ControllerInput.AirBrakeRight:=Min(Max(FFNN.Outputs[4],0.0),1.0);
end else begin
FillChar(ControllerInput,SizeOf(TGameShipControllerInput),#0);
end;
end;
So the FFNN-based AI simulates just human analog-controller input. And the training code is:
Code:
procedure TrainAI;
const TeachIterations=1024;
var i,Inputs,Hidden,Outputs,TeachIteration,Index,SubIndex,TimeStatesIndex,i32,
Counter:longint;
TimeStates:array of TGameShipTimeStates;
TimeState:PGameShipTimeState;
pn,fn:ansistring;
fs:TFileStream;
FFNN:TGameFFNN;
begin
FFNN:=nil;
TimeStates:=nil;
try
{$ifdef FewerRays}
Inputs:=2+CountSensorRays+CountSensorOpponentRays;
{$else}
Inputs:=2+CountSensorWallRays+CountSensorFloorRays+CountSensorCeilRays+CountSensorOpponentRays;
{$endif}
Outputs:=3;
Hidden:=(Inputs+Outputs)*4;
FFNN:=TGameFFNN.Create([Inputs,Hidden,Hidden,Outputs],0.3,0.9);
SetLength(TimeStates,0);
pn:=IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0)))+'aitrainingdata';
if DirectoryExists(pn) then begin
pn:=IncludeTrailingPathDelimiter(pn);
i:=0;
repeat
fn:=pn+'dataset'+IntToStr(i)+'.bin';
if FileExists(fn) then begin
inc(i);
end else begin
break;
end;
until i>$40000000;
SetLength(TimeStates,i);
i:=0;
repeat
fn:=pn+'dataset'+IntToStr(i)+'.bin';
if FileExists(fn) then begin
if (i+1)>length(TimeStates) then begin
SetLength(TimeStates,i+1);
end;
fs:=TFileStream.Create(fn,fmOpenRead);
try
if fs.Read(i32,SizeOf(longint))=SizeOf(longint) then begin
TimeStates[i].Count:=Max(0,i32);
SetLength(TimeStates[i].Items,TimeStates[i].Count);
if TimeStates[i].Count>0 then begin
for Index:=0 to TimeStates[i].Count-1 do begin
fs.Read(TimeStates[i].Items[Index].Sensors,SizeOf(TGameShipSensors));
fs.Read(TimeStates[i].Items[Index].ControllerInput,SizeOf(TGameShipControllerInput));
end;
end;
end;
finally
fs.Free;
end;
inc(i);
end else begin
break;
end;
until i>$40000000;
for TeachIteration:=1 to TeachIterations do begin
write(#13,'Training AI... ',(TeachIteration/TeachIterations)*100.0:6:2,' %');
for TimeStatesIndex:=0 to length(TimeStates)-1 do begin
for Index:=0 to TimeStates[TimeStatesIndex].Count-1 do begin
TimeState:=@TimeStates[TimeStatesIndex].Items[Index];
begin
FFNN.Inputs[0]:=TimeState^.Sensors.WayPointAngle;
FFNN.Inputs[1]:=TimeState^.Sensors.Speed;
SubIndex:=2;
{$ifdef FewerRays}
for Counter:=0 to CountSensorRays-1 do begin
FFNN.Inputs[SubIndex]:=TimeState^.Sensors.Rays[Counter];
inc(SubIndex);
end;
{$else}
for Counter:=0 to CountSensorWallRays-1 do begin
FFNN.Inputs[SubIndex]:=TimeState^.Sensors.WallRays[Counter];
inc(SubIndex);
end;
for Counter:=0 to CountSensorFloorRays-1 do begin
FFNN.Inputs[SubIndex]:=TimeState^.Sensors.FloorRays[Counter];
inc(SubIndex);
end;
for Counter:=0 to CountSensorCeilRays-1 do begin
FFNN.Inputs[SubIndex]:=TimeState^.Sensors.CeilRays[Counter];
inc(SubIndex);
end;
{$endif}
for Counter:=0 to CountSensorOpponentRays-1 do begin
FFNN.Inputs[SubIndex]:=TimeState^.Sensors.OpponentRays[Counter];
inc(SubIndex);
end;
end;
FFNN.Calculate;
begin
FFNN.DesiredOutputs[0]:=Min(Max((TimeState^.ControllerInput.Steering+1.0)*0.5,0.0),1.0);
FFNN.DesiredOutputs[1]:=Min(Max((TimeState^.ControllerInput.Accelerate+1.0)*0.5,0.0),1.0);
FFNN.DesiredOutputs[2]:=Min(Max((TimeState^.ControllerInput.Pitch+1.0)*0.5,0.0),1.0);
FFNN.DesiredOutputs[3]:=Min(Max(TimeState^.ControllerInput.AirBrakeLeft,0.0),1.0);
FFNN.DesiredOutputs[4]:=Min(Max(TimeState^.ControllerInput.AirBrakeRight,0.0),1.0);
end;
FFNN.BackPropagate;
end;
end;
write(#13,'Training AI... ');
write(#13,'Training AI... ');
fn:=IncludeTrailingPathDelimiter(IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0)))+'assets')+'aibrain.bin';
fs:=TFileStream.Create(fn,fmCreate);
try
FFNN.SaveToStream(fs);
finally
fs.Free;
end;
end;
end;
finally
FFNN.Free;
SetLength(TimeStates,0);
end;
end;
Overall very simple, but it works very well for the small size and simplicity of the code, since the FFNN-based AI is driving almost without any accident and in a very human-style behaviour way (since the recorded training datasets are by-human-drived track rounds).
And Supraleiter supports already most all input possibles, keyboard, gamepad controller, touchscreen (but due to a SDL 2.0 bug, only under Win7, with Win8(.1) has SDL 2.0 some problems yet, due to some small multitouch system API changes from Win7 to Win, accelometer, mouse, touchpad, etc. etc. etc,.
Bookmarks