The reason is due to the way array size is determined. Array size is equal to the greatest index in the array. If the 20th car hits a wall before other cars are finished, it will end the generation, leaving unfinished car scores as ‘void’, another word for nil (not sure why Roblox didn’t use ‘nil’ here but whatever).
Instead, you need to keep a counter for how many cars have finished, ‘numFinished’, and replace #scoreTable with it.
Whenever a car submits it’s score, just increment this counter by 1.
With this, 3 tiny lines of code are edited. Nothing else is changed.
local PhysicsService = game:GetService("PhysicsService")
local Package = game:GetService("ReplicatedStorage").NNLibrary
local FeedforwardNetwork = require(Package.NeuralNetwork.FeedforwardNetwork)
local ParamEvo = require(Package.GeneticAlgorithm.ParamEvo)
local clock = os.clock()
local collisionGroupName = "CarCollisionsDisabled"
-- Setup CollisionGroup for cars
PhysicsService:CreateCollisionGroup(collisionGroupName)
PhysicsService:CollisionGroupSetCollidable(collisionGroupName, collisionGroupName, false)
-- Function to activate scripts inside of car
local function setupCar(car)
car.RemoteControl.Torque = 1000
car.RemoteControl.Throttle = 5
car.RemoteControl.TurnSpeed = 30
for _, item in pairs(car:GetDescendants()) do
if item:IsA("BasePart") or item:IsA("UnionOperation") then
PhysicsService:SetPartCollisionGroup(item, collisionGroupName)
end
end
-- Create ray visualizers
local ray = Instance.new("Part")
ray.CanCollide = false
ray.Massless = true
ray.CFrame = car.RaycastPart.CFrame
ray.Size = Vector3.new(1, 1, 1)
ray.Color = Color3.fromRGB(255, 0, 0)
ray.Name = "frontRay"
ray.Parent = car
local weld = Instance.new("WeldConstraint")
weld.Part0 = car.RaycastPart
weld.Part1 = ray
weld.Parent = ray
end
-- Setup car
local carSource = game:GetService("ServerStorage").Car
setupCar(carSource)
-- Function that casts rays from the car in five directions and returns the distances in a table
local function getRayDistances(car)
-- Setup RayCastParams
local rayCastParams = RaycastParams.new()
rayCastParams.IgnoreWater = true
rayCastParams.FilterType = Enum.RaycastFilterType.Whitelist
rayCastParams.FilterDescendantsInstances = {workspace.Walls}
local bumper = car.RaycastPart
local bumperPos = bumper.Position
local dist = 250
local FrontRay = workspace:Raycast(bumperPos, bumper.CFrame.RightVector * dist, rayCastParams) -- Cast ray for front
local FrontLeftRay = workspace:Raycast(bumperPos, Vector3.new(bumper.CFrame.RightVector.X, 0, bumper.CFrame.LookVector.Z) * dist, rayCastParams) -- Cast ray to frontLeft
local FrontRightRay = workspace:Raycast(bumperPos, Vector3.new(bumper.CFrame.RightVector.X, 0, -bumper.CFrame.LookVector.Z) * dist, rayCastParams) -- Cast ray to frontRight
local LeftRay = workspace:Raycast(bumperPos, bumper.CFrame.LookVector * dist, rayCastParams) -- Cast ray to left
local RightRay = workspace:Raycast(bumperPos, -bumper.CFrame.LookVector * dist, rayCastParams) -- Cast ray to right
local pos1 = 1
local pos2 = 1
local pos3 = 1
local pos4 = 1
local pos5 = 1
if FrontRay then
pos1 = ((FrontRay.Position - bumperPos).Magnitude) / dist
end
if FrontLeftRay then
pos2 = ((FrontLeftRay.Position - bumperPos).Magnitude) / dist
end
if FrontRightRay then
pos3 = ((FrontRightRay.Position - bumperPos).Magnitude) / dist
end
if LeftRay then
pos4 = ((LeftRay.Position - bumperPos).Magnitude) / dist
end
if RightRay then
pos5 = ((RightRay.Position - bumperPos).Magnitude) / dist
end
--local calc = pos1*dist
--local ray = car.frontRay
--ray.Size = Vector3.new(calc, 1, 1)
--ray.Position = Vector3.new(bumperPos.X + calc/2, ray.Position.Y, ray.Position.Z)
return {front = pos1, frontLeft = pos2, frontRight = pos3, left = pos4, right = pos5}
end
-- Settings for genetic algorithm
local geneticSetting = {
--[[ The function that, when given the network, will return it's score.
ScoreFunction = function(net)
local startTime = os.clock()
local clone = game:GetService("ServerStorage").Car:Clone()
clone.RemoteControl.MaxSpeed = 200
-- Parent to workspace and then setup Scripts of car
clone.Parent = workspace
local score = 0
local bool = true
local checkpointsHit = {}
for _, v in pairs(clone:GetDescendants()) do
if v:IsA("BasePart") and v.CanCollide == true then
v.Touched:Connect(function(hit)
if hit.Parent.Parent == workspace.Walls then -- Destroy car on hit of walls
bool = false
elseif hit.Parent == workspace.Checkpoints and not checkpointsHit[tonumber(hit.Name)] then -- Give extra points when car reaches checkpoint
local numHit = tonumber(hit.Name)
score += (numHit * 2)
checkpointsHit[numHit] = hit
end
end)
end
end
while bool do
local distances = getRayDistances(clone) -- Get Distances of rays
local output = net(distances) -- Get output of NN with input distances
-- Set steering direction to direction of NN
clone.RemoteControl.SteerFloat = output.steerDirection
-- Set speed of car
--clone.RemoteControl.MaxSpeed = math.abs(output.speed) * 300
-- Check if this simulation has been running for longer than x seconds
if os.clock() > startTime + 90 then
score -= 40 -- Punish algorithm
break
end
wait()
end
score += (os.clock() - startTime)/2 -- Increment score based on time alive (longer is better)
print("Exit score: "..math.floor(score*100)/100)
clone:Destroy()
return score
end;]]
-- The function that runs when a generation is complete. It is given the genetic algorithm as input.
PostFunction = function(geneticAlgo)
local info = geneticAlgo:GetInfo()
print("Generation " .. info.Generation .. ", Best Score: " .. info.BestScore)
end;
HigherScoreBetter = true;
PercentageToKill = 0.4;
PercentageOfKilledToRandomlySpare = 0.1;
PercentageOfBestParentToCrossover = 0.8;
PercentageToMutate = 0.8;
MutateBestNetwork = true;
PercentageOfCrossedToMutate = 0.6;
--NumberOfNodesToMutate = 3;
--ParameterMutateRange = 3;
}
local feedForwardSettings = {
HiddenActivationName = "ReLU";
OutputActivationName = "Tanh";
--Bias = 0;
--LearningRate = 0.1;
--RandomizeWeights = true;
}
-- Create a new network with 5 inputs, 2 layers with 4 nodes each and 1 output "steerDirection"
local tempNet = FeedforwardNetwork.new({"front", "frontLeft", "frontRight", "left", "right"}, 2, 4, {"steerDirection"}, feedForwardSettings)
--local tempNet = FeedforwardNetwork.newFromSave(game.ServerStorage.NetworkSave.Value)
local populationSize = 20
local geneticAlgo = ParamEvo.new(tempNet, populationSize, geneticSetting) -- Create ParamEvo with the tempNet template, population size and settings
local scoreTable = {}
local generations = 30 -- Number of generations to train network with
for _ = 1, generations do
local numFinished = 0
for index = 1, populationSize do
local newThread = coroutine.create(function()
local startTime = os.clock()
local clone = game:GetService("ServerStorage").Car:Clone()
clone.RemoteControl.MaxSpeed = 200
-- Parent to workspace and then setup Scripts of car
clone.Parent = workspace
local score = 0
local bool = true
local checkpointsHit = {}
for _, v in pairs(clone:GetDescendants()) do
if v:IsA("BasePart") and v.CanCollide == true then
v.Touched:Connect(function(hit)
if hit.Parent.Parent == workspace.Walls then -- Destroy car on hit of walls
bool = false
elseif hit.Parent == workspace.Checkpoints and not checkpointsHit[tonumber(hit.Name)] then -- Give extra points when car reaches checkpoint
local numHit = tonumber(hit.Name)
if numHit and typeof(numHit) == "number" then
score += (numHit * 2)
checkpointsHit[numHit] = hit
end
end
end)
end
end
-- Setup Algorithm
local net = geneticAlgo:GetPopulation()[index].Network
while bool do
local distances = getRayDistances(clone) -- Get Distances of rays
local output = net(distances) -- Get output of NN with input distances
-- Set steering direction to direction of NN
clone.RemoteControl.SteerFloat = output.steerDirection
-- Set speed of car
--clone.RemoteControl.MaxSpeed = math.abs(output.speed) * 300
-- Check if this simulation has been running for longer than x seconds
if os.clock() > startTime + 90 then
score -= 40 -- Punish algorithm
break
end
wait()
end
clone:Destroy()
score = score + (os.clock() - startTime)/2 -- Increment score based on time alive (longer is better)
--print("Exit score: " .. score)
scoreTable[index] = score
numFinished += 1
end)
coroutine.resume(newThread)
wait(0.5)
end
-- Wait until generation finished
while numFinished < populationSize do
wait(1)
end
print(scoreTable)
geneticAlgo:ProcessGeneration(scoreTable)
scoreTable = {}
print(scoreTable)
end
local save = geneticAlgo:GetBestNetwork():Save()
game.ServerStorage.NetworkSave.Value = save
print(save)
--[[ * Code for running network
for i = 1, 20 do
local clone = game:GetService("ServerStorage").Car:Clone()
clone.RemoteControl.MaxSpeed = 200
clone.Parent = workspace
local bool = true
for _, v in pairs(clone:GetDescendants()) do
if v:IsA("BasePart") and v.CanCollide == true then
v.Touched:Connect(function(hit)
if hit.Parent.Parent == workspace.Walls then -- Destroy car on hit of walls
bool = false
end
end)
end
end
while bool do
local distances = getRayDistances(clone) -- Get Distances of rays
local output = tempNet(distances) -- Get output of NN with input distances
-- Set steering direction to direction of NN
clone.RemoteControl.SteerFloat = output.steerDirection
wait()
end
clone:Destroy()
end]]