mirror of
https://gitea.wildfiregames.com/0ad/0ad.git
synced 2026-07-20 20:26:36 +00:00
4df03ed2d2
This is a paradigm change for AI computation. Historically, the AI was intended to be run in a separate thread from the simulation. The idea was that slow AI wouldn't stop the renderer from being smooth. In that original design, the AI received a copy of the game world and used that to run its logic. This meant the simulation could safely do whatever it wanted in the meantime. This copy was done via AIProxy & AIInterface. This design ended up having significant flaws: - The copying impacts the simulation negatively, particularly because AIProxy subscribes to a lot of messages (sometimes sent exclusively to it). This time cannot be threaded, and impacts MP games without AIs. - Copying the data is increasingly difficult. Modifiers are a headache, LOS is not implemented. Lots of logic is duplicated. The intended benefits of the design also failed to realise somewhat: - The AI was never threaded, and in fact, it is probably better to try and thread Sim + AI from the renderer than just the AI, at which point threading the AI specifically brings little benefit. The new design is much simpler and straighforward, but this has some side-effects: - The AI can now change the simulation. This can be used for cheating, or possibly for a tutorial AI. - The AI runs in the same GC zone as the simulation, which may lead to more frequent Sim GCs (but overall we might expect a reduction in temporary objects). - The AI state was essentially cached, so replacing some functions with Engine.QueryInterface might be slower. The tradeoff should be balanced by lower AIProxy computation times. Future work: - Threading some specific AI tasks could still be worthwhile, but should be done in specific worker threads, allowed to run over several turns if needed. Technical note: the AI 'global' is in its own Realm, which means name collisions with the same are not possible. Other notes: - The RL Interface uses the AI Interface and thus will gradually lose some data there. Given that the RL Interface can now request data however, this should be dine. Refs #5962, #2370 Differential Revision: https://code.wildfiregames.com/D3769 This was SVN commit r26274.
372 lines
9.7 KiB
JavaScript
372 lines
9.7 KiB
JavaScript
function AIProxy() {}
|
|
|
|
AIProxy.prototype.Schema =
|
|
"<empty/>";
|
|
|
|
/**
|
|
* AIProxy passes its entity's state data to AI scripts.
|
|
*
|
|
* Efficiency is critical: there can be many thousands of entities,
|
|
* and the data returned by this component is serialized and copied to
|
|
* the AI thread every turn, so it can be quite expensive.
|
|
*
|
|
* We omit all data that can be derived statically from the template XML
|
|
* files - the AI scripts can parse the templates themselves.
|
|
* This violates the component interface abstraction and is potentially
|
|
* fragile if the template formats change (since both the component code
|
|
* and the AI will have to be updated in sync), but it's not *that* bad
|
|
* really and it helps performance significantly.
|
|
*
|
|
* We also add an optimisation to avoid copying non-changing values.
|
|
* The first call to GetRepresentation calls GetFullRepresentation,
|
|
* which constructs the complete entity state representation.
|
|
* After that, we simply listen to events from the rest of the gameplay code,
|
|
* and store the changed data in this.changes.
|
|
* Properties in this.changes will override those previously returned
|
|
* from GetRepresentation; if a property isn't overridden then the AI scripts
|
|
* will keep its old value.
|
|
*
|
|
* The event handlers should set this.changes.whatever to exactly the
|
|
* same as GetFullRepresentation would set.
|
|
*/
|
|
|
|
AIProxy.prototype.Init = function()
|
|
{
|
|
this.changes = null;
|
|
this.needsFullGet = true;
|
|
this.cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface);
|
|
};
|
|
|
|
AIProxy.prototype.Serialize = null; // we have no dynamic state to save
|
|
|
|
AIProxy.prototype.Deserialize = function()
|
|
{
|
|
this.Init();
|
|
};
|
|
|
|
AIProxy.prototype.GetRepresentation = function()
|
|
{
|
|
// Return the full representation the first time we're called
|
|
let ret;
|
|
if (this.needsFullGet)
|
|
ret = this.GetFullRepresentation();
|
|
else
|
|
ret = this.changes;
|
|
|
|
// Initialise changes to null instead of {}, to avoid memory allocations in the
|
|
// common case where there will be no changes; event handlers should each reset
|
|
// it to {} if needed
|
|
this.changes = null;
|
|
|
|
return ret;
|
|
};
|
|
|
|
AIProxy.prototype.NotifyChange = function()
|
|
{
|
|
if (this.needsFullGet)
|
|
{
|
|
// not yet notified, be sure that the owner is set before doing so
|
|
// as the Create event is sent only on first ownership changed
|
|
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
|
|
if (!cmpOwnership || cmpOwnership.GetOwner() < 0)
|
|
return false;
|
|
}
|
|
|
|
if (!this.changes)
|
|
{
|
|
this.changes = {};
|
|
this.cmpAIInterface.ChangedEntity(this.entity);
|
|
}
|
|
return true;
|
|
};
|
|
|
|
// AI representation-updating event handlers:
|
|
|
|
AIProxy.prototype.OnPositionChanged = function(msg)
|
|
{
|
|
if (!this.NotifyChange())
|
|
return;
|
|
|
|
if (msg.inWorld)
|
|
{
|
|
this.changes.position = [msg.x, msg.z];
|
|
this.changes.angle = msg.a;
|
|
}
|
|
else
|
|
{
|
|
this.changes.position = undefined;
|
|
this.changes.angle = undefined;
|
|
}
|
|
};
|
|
|
|
AIProxy.prototype.OnHealthChanged = function(msg)
|
|
{
|
|
if (!this.NotifyChange())
|
|
return;
|
|
this.changes.hitpoints = msg.to;
|
|
};
|
|
|
|
AIProxy.prototype.OnGarrisonedStateChanged = function(msg)
|
|
{
|
|
if (!this.NotifyChange())
|
|
return;
|
|
this.changes.garrisonHolderID = msg.holderID;
|
|
};
|
|
|
|
AIProxy.prototype.OnCapturePointsChanged = function(msg)
|
|
{
|
|
if (!this.NotifyChange())
|
|
return;
|
|
this.changes.capturePoints = msg.capturePoints;
|
|
};
|
|
|
|
AIProxy.prototype.OnInvulnerabilityChanged = function(msg)
|
|
{
|
|
if (!this.NotifyChange())
|
|
return;
|
|
this.changes.invulnerability = msg.invulnerability;
|
|
};
|
|
|
|
AIProxy.prototype.OnUnitIdleChanged = function(msg)
|
|
{
|
|
if (!this.NotifyChange())
|
|
return;
|
|
this.changes.idle = msg.idle;
|
|
};
|
|
|
|
AIProxy.prototype.OnUnitStanceChanged = function(msg)
|
|
{
|
|
if (!this.NotifyChange())
|
|
return;
|
|
this.changes.stance = msg.to;
|
|
};
|
|
|
|
AIProxy.prototype.OnUnitAIStateChanged = function(msg)
|
|
{
|
|
if (!this.NotifyChange())
|
|
return;
|
|
this.changes.unitAIState = msg.to;
|
|
};
|
|
|
|
AIProxy.prototype.OnUnitAIOrderDataChanged = function(msg)
|
|
{
|
|
if (!this.NotifyChange())
|
|
return;
|
|
this.changes.unitAIOrderData = msg.to;
|
|
};
|
|
|
|
AIProxy.prototype.OnProductionQueueChanged = function(msg)
|
|
{
|
|
if (!this.NotifyChange())
|
|
return;
|
|
let cmpProductionQueue = Engine.QueryInterface(this.entity, IID_ProductionQueue);
|
|
this.changes.trainingQueue = cmpProductionQueue.GetQueue();
|
|
};
|
|
|
|
AIProxy.prototype.OnGarrisonedUnitsChanged = function(msg)
|
|
{
|
|
if (!this.NotifyChange())
|
|
return;
|
|
|
|
let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
|
|
this.changes.garrisoned = cmpGarrisonHolder.GetEntities();
|
|
|
|
// Send a message telling a unit garrisoned or ungarrisoned.
|
|
// I won't check if the unit is still alive so it'll be up to the AI.
|
|
for (let ent of msg.added)
|
|
this.cmpAIInterface.PushEvent("Garrison", { "entity": ent, "holder": this.entity });
|
|
for (let ent of msg.removed)
|
|
this.cmpAIInterface.PushEvent("UnGarrison", { "entity": ent, "holder": this.entity });
|
|
};
|
|
|
|
AIProxy.prototype.OnFoundationProgressChanged = function(msg)
|
|
{
|
|
if (!this.NotifyChange())
|
|
return;
|
|
this.changes.foundationProgress = msg.to;
|
|
};
|
|
|
|
AIProxy.prototype.OnFoundationBuildersChanged = function(msg)
|
|
{
|
|
if (!this.NotifyChange())
|
|
return;
|
|
this.changes.foundationBuilders = msg.to;
|
|
};
|
|
|
|
AIProxy.prototype.OnDropsiteSharingChanged = function(msg)
|
|
{
|
|
if (!this.NotifyChange())
|
|
return;
|
|
this.changes.sharedDropsite = msg.shared;
|
|
};
|
|
|
|
AIProxy.prototype.OnTerritoryDecayChanged = function(msg)
|
|
{
|
|
if (!this.NotifyChange())
|
|
return;
|
|
this.changes.decaying = msg.to;
|
|
this.cmpAIInterface.PushEvent("TerritoryDecayChanged", msg);
|
|
};
|
|
|
|
// TODO: event handlers for all the other things
|
|
|
|
AIProxy.prototype.GetFullRepresentation = function()
|
|
{
|
|
this.needsFullGet = false;
|
|
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
|
|
|
|
let ret = {
|
|
// These properties are constant and won't need to be updated
|
|
"id": this.entity,
|
|
"template": cmpTemplateManager.GetCurrentTemplateName(this.entity)
|
|
};
|
|
|
|
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
|
|
if (cmpPosition)
|
|
{
|
|
// Updated by OnPositionChanged
|
|
|
|
if (cmpPosition.IsInWorld())
|
|
{
|
|
let pos = cmpPosition.GetPosition2D();
|
|
ret.position = [pos.x, pos.y];
|
|
ret.angle = cmpPosition.GetRotation().y;
|
|
}
|
|
else
|
|
{
|
|
ret.position = undefined;
|
|
ret.angle = undefined;
|
|
}
|
|
}
|
|
|
|
let cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
|
|
if (cmpHealth)
|
|
{
|
|
// Updated by OnHealthChanged
|
|
ret.hitpoints = cmpHealth.GetHitpoints();
|
|
}
|
|
|
|
let cmpResistance = Engine.QueryInterface(this.entity, IID_Resistance);
|
|
if (cmpResistance)
|
|
ret.invulnerability = cmpResistance.IsInvulnerable();
|
|
|
|
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
|
|
if (cmpOwnership)
|
|
{
|
|
// Updated by OnOwnershipChanged
|
|
ret.owner = cmpOwnership.GetOwner();
|
|
}
|
|
|
|
let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
|
|
if (cmpUnitAI)
|
|
{
|
|
// Updated by OnUnitIdleChanged
|
|
ret.idle = cmpUnitAI.IsIdle();
|
|
// Updated by OnUnitStanceChanged
|
|
ret.stance = cmpUnitAI.GetStanceName();
|
|
// Updated by OnUnitAIStateChanged
|
|
ret.unitAIState = cmpUnitAI.GetCurrentState();
|
|
// Updated by OnUnitAIOrderDataChanged
|
|
ret.unitAIOrderData = cmpUnitAI.GetOrderData();
|
|
}
|
|
|
|
let cmpProductionQueue = Engine.QueryInterface(this.entity, IID_ProductionQueue);
|
|
if (cmpProductionQueue)
|
|
{
|
|
// Updated by OnProductionQueueChanged
|
|
ret.trainingQueue = cmpProductionQueue.GetQueue();
|
|
}
|
|
|
|
let cmpFoundation = Engine.QueryInterface(this.entity, IID_Foundation);
|
|
if (cmpFoundation)
|
|
{
|
|
// Updated by OnFoundationProgressChanged
|
|
ret.foundationProgress = cmpFoundation.GetBuildPercentage();
|
|
}
|
|
|
|
let cmpResourceDropsite = Engine.QueryInterface(this.entity, IID_ResourceDropsite);
|
|
if (cmpResourceDropsite)
|
|
{
|
|
// Updated by OnDropsiteSharingChanged
|
|
ret.sharedDropsite = cmpResourceDropsite.IsShared();
|
|
}
|
|
|
|
let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
|
|
if (cmpGarrisonHolder)
|
|
{
|
|
// Updated by OnGarrisonedUnitsChanged
|
|
ret.garrisoned = cmpGarrisonHolder.GetEntities();
|
|
}
|
|
|
|
let cmpGarrisonable = Engine.QueryInterface(this.entity, IID_Garrisonable);
|
|
if (cmpGarrisonable)
|
|
{
|
|
// Updated by OnGarrisonedStateChanged
|
|
ret.garrisonHolderID = cmpGarrisonable.HolderID();
|
|
}
|
|
|
|
let cmpTerritoryDecay = Engine.QueryInterface(this.entity, IID_TerritoryDecay);
|
|
if (cmpTerritoryDecay)
|
|
ret.decaying = cmpTerritoryDecay.IsDecaying();
|
|
|
|
let cmpCapturable = Engine.QueryInterface(this.entity, IID_Capturable);
|
|
if (cmpCapturable)
|
|
ret.capturePoints = cmpCapturable.GetCapturePoints();
|
|
|
|
return ret;
|
|
};
|
|
|
|
// AI event handlers:
|
|
// (These are passed directly as events to the AI scripts, rather than updating
|
|
// our proxy representation.)
|
|
// (This shouldn't include extremely high-frequency events, like PositionChanged,
|
|
// because that would be very expensive and AI will rarely care about all those
|
|
// events.)
|
|
|
|
// special case: this changes the state and sends an event.
|
|
AIProxy.prototype.OnOwnershipChanged = function(msg)
|
|
{
|
|
this.NotifyChange();
|
|
|
|
if (msg.from == INVALID_PLAYER)
|
|
{
|
|
this.cmpAIInterface.PushEvent("Create", { "entity": msg.entity });
|
|
return;
|
|
}
|
|
if (msg.to == INVALID_PLAYER)
|
|
{
|
|
this.cmpAIInterface.PushEvent("Destroy", { "entity": msg.entity });
|
|
return;
|
|
}
|
|
|
|
this.changes.owner = msg.to;
|
|
this.cmpAIInterface.PushEvent("OwnershipChanged", msg);
|
|
};
|
|
|
|
AIProxy.prototype.OnAttacked = function(msg)
|
|
{
|
|
this.cmpAIInterface.PushEvent("Attacked", msg);
|
|
};
|
|
|
|
AIProxy.prototype.OnConstructionFinished = function(msg)
|
|
{
|
|
this.cmpAIInterface.PushEvent("ConstructionFinished", msg);
|
|
};
|
|
|
|
AIProxy.prototype.OnTrainingStarted = function(msg)
|
|
{
|
|
this.cmpAIInterface.PushEvent("TrainingStarted", msg);
|
|
};
|
|
|
|
AIProxy.prototype.OnTrainingFinished = function(msg)
|
|
{
|
|
this.cmpAIInterface.PushEvent("TrainingFinished", msg);
|
|
};
|
|
|
|
AIProxy.prototype.OnAIMetadata = function(msg)
|
|
{
|
|
this.cmpAIInterface.PushEvent("AIMetadata", msg);
|
|
};
|
|
|
|
Engine.RegisterComponentType(IID_AIProxy, "AIProxy", AIProxy);
|