Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 48 additions & 5 deletions source/EngineGpuKernels/SensorProcessor.cuh
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ private:
__inline__ __device__ static uint64_t pack(float distance, float angle, float density, uint16_t misc = 0);
__inline__ __device__ static void unpack(float& distance, float& angle, float& density, uint16_t& misc, uint64_t bytes);

__inline__ __device__ static void writeSignal(Signal& signal, float angle, float density, float distance);
__inline__ __device__ static void writeSignal(Signal& signal, float angle, float density, float distance, float numCellsSignal = 0.0f);

__inline__ __device__ static float convertNumCellsToSignal(uint32_t numCells);
__inline__ __device__ static float lookupCreatureNumCellsSignal(SimulationData& data, float2 const& pos, uint16_t creatureIdPart);

__inline__ __device__ static uint16_t convertAngleToUint16(float angle);
__inline__ __device__ static float convertUint16ToAngle(uint16_t b);
Expand Down Expand Up @@ -203,12 +206,19 @@ __inline__ __device__ void SensorProcessor::initialScan(SimulationData& data, Si

auto refAngle = Math::angleOfVector(SignalProcessor::calcReferenceDirection(data, cell));
auto relAngle = Math::getNormalizedAngle(absAngle - refAngle - cell->frontAngle, -180.0f);
writeSignal(cell->signal, relAngle, density, distance);
statistics.incNumSensorMatches(cell->color);

auto matchPos = cell->pos + Math::unitVectorOfAngle(absAngle) * distance;
data.cellMap.correctPosition(matchPos);

// For DetectCreature mode, look up the creature at match position to get numCells
float numCellsSignal = 0.0f;
if (cell->cellTypeData.sensor.mode == SensorMode_DetectCreature) {
numCellsSignal = lookupCreatureNumCellsSignal(data, matchPos, creatureIdPart);
}

writeSignal(cell->signal, relAngle, density, distance, numCellsSignal);
statistics.incNumSensorMatches(cell->color);

// No relocation for structures
if (cell->cellTypeData.sensor.mode != SensorMode_DetectStructure) {
cell->cellTypeData.sensor.lastMatchAvailable = true;
Expand Down Expand Up @@ -297,8 +307,16 @@ __inline__ __device__ void SensorProcessor::relocateLastMatch(SimulationData& da
}

auto targetPos = cell->pos + Math::unitVectorOfAngle(absAngle) * distance;
data.cellMap.correctPosition(targetPos);
auto relAngle = Math::getNormalizedAngle(absAngle - refAngle - cell->frontAngle, -180.0f);
writeSignal(cell->signal, relAngle, density, distance);

// For DetectCreature mode, look up the creature at target position to get numCells
float numCellsSignal = 0.0f;
if (cell->cellTypeData.sensor.mode == SensorMode_DetectCreature) {
numCellsSignal = lookupCreatureNumCellsSignal(data, targetPos, creatureIdPart);
}

writeSignal(cell->signal, relAngle, density, distance, numCellsSignal);

statistics.incNumSensorMatches(cell->color);

Expand Down Expand Up @@ -422,12 +440,37 @@ __inline__ __device__ void SensorProcessor::unpack(float& distance, float& angle
misc = static_cast<int16_t>(bytes & 0xFFFF);
}

__inline__ __device__ void SensorProcessor::writeSignal(Signal& signal, float angle, float density, float distance)
__inline__ __device__ void SensorProcessor::writeSignal(Signal& signal, float angle, float density, float distance, float numCellsSignal)
{
signal.channels[Channels::SensorFoundResult] = 1; // Something found
signal.channels[Channels::SensorAngle] = angle / 180.0f; // Angle: between -1.0 and 1.0
signal.channels[Channels::SensorDensity] = min(1.0f, density); // Normalized density (1.0 = 64 cells in 8x8 region)
signal.channels[Channels::SensorDistance] = 1.0f - min(1.0f, distance / 256); // Distance: 1 = close, 0 = far away
signal.channels[Channels::SensorNumCells] = numCellsSignal; // Number of cells in detected creature (non-linear scale)
}

// Convert numCells to a non-linear signal value in range [0, 1]
// Formula: log2(numCells) / 7.0, clamped to [0, 1]
// Examples: 2 cells -> ~0.14, 30 cells -> ~0.70, 60 cells -> ~0.85, 120 cells -> ~0.99
__inline__ __device__ float SensorProcessor::convertNumCellsToSignal(uint32_t numCells)
{
if (numCells <= 1) {
return 0.0f;
}
return min(1.0f, log2f(toFloat(numCells)) / 7.0f);
}

// Look up the creature at the given position and return its numCells as a signal value
__inline__ __device__ float SensorProcessor::lookupCreatureNumCellsSignal(SimulationData& data, float2 const& pos, uint16_t creatureIdPart)
{
auto otherCell = data.cellMap.getFirst(pos);
while (otherCell != nullptr) {
if (otherCell->creature != nullptr && (otherCell->creature->id & 0xffff) == creatureIdPart) {
return convertNumCellsToSignal(otherCell->creature->numCells);
}
otherCell = otherCell->nextCell;
}
return 0.0f;
}

__inline__ __device__ uint16_t SensorProcessor::convertAngleToUint16(float angle)
Expand Down
171 changes: 171 additions & 0 deletions source/EngineTests/SensorTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1372,3 +1372,174 @@ TEST_F(SensorTests, telemetry_allOutputs)
EXPECT_TRUE(velStrength > 0.5f);
EXPECT_TRUE(velStrength < 0.7f);
}

/**
* Tests for SensorNumCells output in DetectCreature mode
*/
TEST_F(SensorTests, detectCreature_numCellsOutput_smallCreature)
{
auto data = Description().addCreature(CreatureDescription().id(0).cells({
CellDescription().id(1).pos({100.0f, 100.0f}).frontAngle(0.0f).cellType(SensorDescription().autoTriggerInterval(3).mode(DetectCreatureDescription())),
CellDescription().id(2).pos({101.0f, 100.0f}),
}));
data.addConnection(1, 2);

// Create a small target creature with 4 cells
data.addCreature(CreatureDescription().id(1).cells({
CellDescription().id(10).pos({100.0f, 80.0f}),
CellDescription().id(11).pos({101.0f, 80.0f}),
CellDescription().id(12).pos({100.0f, 81.0f}),
CellDescription().id(13).pos({101.0f, 81.0f}),
}));

_simulationFacade->setSimulationData(data);
_simulationFacade->calcTimesteps(1);

auto actualData = _simulationFacade->getSimulationData();
auto actualSensor = actualData.getCellRef(1);

EXPECT_TRUE(actualSensor._signalState == SignalState_Active);
EXPECT_TRUE(approxCompare(1.0f, actualSensor._signal._channels[Channels::SensorFoundResult]));

// 4 cells: log2(4) / 7.0 = 2 / 7 ≈ 0.286
auto numCellsSignal = actualSensor._signal._channels[Channels::SensorNumCells];
EXPECT_TRUE(numCellsSignal > 0.2f);
EXPECT_TRUE(numCellsSignal < 0.4f);
}

TEST_F(SensorTests, detectCreature_numCellsOutput_largeCreature)
{
auto data = Description().addCreature(CreatureDescription().id(0).cells({
CellDescription().id(1).pos({100.0f, 100.0f}).frontAngle(0.0f).cellType(SensorDescription().autoTriggerInterval(3).mode(DetectCreatureDescription())),
CellDescription().id(2).pos({101.0f, 100.0f}),
}));
data.addConnection(1, 2);

// Create a large creature with 100 cells (10x10 grid) using helper
data.add(createLargeCreature());

_simulationFacade->setSimulationData(data);
_simulationFacade->calcTimesteps(1);

auto actualData = _simulationFacade->getSimulationData();
auto actualSensor = actualData.getCellRef(1);

EXPECT_TRUE(actualSensor._signalState == SignalState_Active);
EXPECT_TRUE(approxCompare(1.0f, actualSensor._signal._channels[Channels::SensorFoundResult]));

// 100 cells: log2(100) / 7.0 ≈ 6.64 / 7 ≈ 0.95
auto numCellsSignal = actualSensor._signal._channels[Channels::SensorNumCells];
EXPECT_TRUE(numCellsSignal > 0.85f);
EXPECT_TRUE(numCellsSignal < 1.0f);
}

TEST_F(SensorTests, detectCreature_numCellsOutput_varyingSizes)
{
// Test that larger creatures produce higher numCells signals
auto runTest = [this](int creatureSize) {
auto data = Description().addCreature(CreatureDescription().id(0).cells({
CellDescription().id(1).pos({100.0f, 100.0f}).frontAngle(0.0f).cellType(SensorDescription().autoTriggerInterval(3).mode(DetectCreatureDescription())),
CellDescription().id(2).pos({101.0f, 100.0f}),
}));
data.addConnection(1, 2);

// Create target creature with specified number of cells
std::vector<CellDescription> targetCells;
for (int i = 0; i < creatureSize; ++i) {
targetCells.emplace_back(CellDescription().id(10 + i).pos({100.0f + toFloat(i % 10), 80.0f + toFloat(i / 10)}));
}
data.addCreature(CreatureDescription().id(1).cells(targetCells));

_simulationFacade->setSimulationData(data);
_simulationFacade->calcTimesteps(1);

auto actualData = _simulationFacade->getSimulationData();
auto actualSensor = actualData.getCellRef(1);

EXPECT_TRUE(actualSensor._signalState == SignalState_Active);
return actualSensor._signal._channels[Channels::SensorNumCells];
};

auto signal2Cells = runTest(2);
auto signal10Cells = runTest(10);
auto signal50Cells = runTest(50);

// Verify non-linear scaling: larger creatures should have higher signals
EXPECT_TRUE(signal10Cells > signal2Cells);
EXPECT_TRUE(signal50Cells > signal10Cells);

// Verify approximate expected values based on log2(n)/7 formula
// 2 cells: log2(2)/7 = 1/7 ≈ 0.14
EXPECT_TRUE(signal2Cells > 0.1f);
EXPECT_TRUE(signal2Cells < 0.2f);
// 10 cells: log2(10)/7 ≈ 3.32/7 ≈ 0.47
EXPECT_TRUE(signal10Cells > 0.4f);
EXPECT_TRUE(signal10Cells < 0.55f);
// 50 cells: log2(50)/7 ≈ 5.64/7 ≈ 0.81
EXPECT_TRUE(signal50Cells > 0.75f);
EXPECT_TRUE(signal50Cells < 0.9f);
}

TEST_F(SensorTests, detectCreature_numCellsOutput_relocation)
{
// Test that numCells is also output correctly during relocation
auto data = Description().addCreature(CreatureDescription().id(0).cells({
CellDescription().id(1).pos({100.0f, 100.0f}).frontAngle(0.0f).cellType(SensorDescription().autoTriggerInterval(3).mode(DetectCreatureDescription())),
CellDescription().id(2).pos({101.0f, 100.0f}),
}));
data.addConnection(1, 2);

// Create a target creature with 25 cells (5x5 grid)
data.add(createLargeCreature({95.0f, 80.0f}, 5));

_simulationFacade->setSimulationData(data);

// First scan - initial detection
_simulationFacade->calcTimesteps(1);
auto actualData = _simulationFacade->getSimulationData();
auto actualSensor = actualData.getCellRef(1);
EXPECT_TRUE(approxCompare(1.0f, actualSensor._signal._channels[Channels::SensorFoundResult]));
auto initialNumCellsSignal = actualSensor._signal._channels[Channels::SensorNumCells];

// Second scan - relocation
_simulationFacade->calcTimesteps(3); // Wait for next trigger
actualData = _simulationFacade->getSimulationData();
actualSensor = actualData.getCellRef(1);

EXPECT_TRUE(actualSensor._signalState == SignalState_Active);
EXPECT_TRUE(approxCompare(1.0f, actualSensor._signal._channels[Channels::SensorFoundResult]));

// numCells should be similar between initial scan and relocation
// 25 cells: log2(25)/7 ≈ 4.64/7 ≈ 0.66
auto relocationNumCellsSignal = actualSensor._signal._channels[Channels::SensorNumCells];
EXPECT_TRUE(relocationNumCellsSignal > 0.6f);
EXPECT_TRUE(relocationNumCellsSignal < 0.75f);
EXPECT_TRUE(approxCompare(initialNumCellsSignal, relocationNumCellsSignal));
}

TEST_P(SensorTests_AllDetectionModesExceptStructure, nonCreatureModes_numCellsZero)
{
// For non-DetectCreature modes, numCells signal should be 0
if (GetParam() == SensorMode_DetectCreature) {
GTEST_SKIP() << "This test is for non-creature detection modes only";
}

auto data = Description().cells({
CellDescription().id(1).pos({100.0f, 100.0f}).frontAngle(0.0f).cellType(SensorDescription().autoTriggerInterval(3).mode(createModeWithDensity(GetParam()))),
CellDescription().id(2).pos({101.0f, 100.0f}),
});
data.addConnection(1, 2);

// Add detection targets above the sensor
addDetectionTargets(data, GetParam(), {98.0f, 20.0f}, 8);

_simulationFacade->setSimulationData(data);
_simulationFacade->calcTimesteps(1);

auto actualData = _simulationFacade->getSimulationData();
auto actualSensor = actualData.getCellRef(1);

EXPECT_TRUE(approxCompare(1.0f, actualSensor._signal._channels[Channels::SensorFoundResult]));
// For non-creature modes, numCells signal should be 0
EXPECT_TRUE(approxCompare(0.0f, actualSensor._signal._channels[Channels::SensorNumCells]));
}