# Add corpse decay and building collapse.

Make 'delete' command kill the unit instead of destroying it.
Make projectiles vanish after a timeout.
Fix projectile landing positions with low framerates.

This was SVN commit r7837.
This commit is contained in:
Ykkrosh 2010-08-01 17:38:01 +00:00
parent fdb8be2fd5
commit 41ad5bd965
13 changed files with 331 additions and 23 deletions

View File

@ -63,7 +63,12 @@ Health.prototype.SetHitpoints = function(value)
return;
this.hitpoints = Math.max(1, Math.min(this.GetMaxHitpoints(), value));
}
};
Health.prototype.Kill = function()
{
this.Reduce(this.hitpoints);
};
Health.prototype.Reduce = function(amount)
{
@ -88,7 +93,7 @@ Health.prototype.Reduce = function(amount)
{
this.hitpoints -= amount;
}
}
};
Health.prototype.Increase = function(amount)
{
@ -97,7 +102,7 @@ Health.prototype.Increase = function(amount)
return;
this.hitpoints = Math.min(this.hitpoints + amount, this.GetMaxHitpoints());
}
};
//// Private functions ////
@ -106,9 +111,7 @@ Health.prototype.CreateCorpse = function()
// Create a static local version of the current entity
var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var templateName = cmpTempMan.GetCurrentTemplateName(this.entity);
var corpse = Engine.AddLocalEntity("preview|" + templateName);
// (Maybe this should be some kind of "corpse|" instead of "preview|", if we want
// to add things like corpse-removal timers and change the terrain conformance mode)
var corpse = Engine.AddLocalEntity("corpse|" + templateName);
// Copy various parameters so it looks just like us

View File

@ -125,11 +125,17 @@ function ProcessCommand(player, cmd)
break;
case "delete-entity":
// Verify the player owns the unit
var cmpOwnership = Engine.QueryInterface(cmd.entity, IID_Ownership);
var entityOwner = cmpOwnership.GetOwner()
if (!cmpOwnership || cmpOwnership.GetOwner() != player)
break;
if (player == entityOwner)
var cmpHealth = Engine.QueryInterface(cmd.entity, IID_Health);
if (cmpHealth)
cmpHealth.Kill();
else
Engine.DestroyEntity(cmd.entity);
break;
default:

View File

@ -21,9 +21,15 @@
</Resources>
</Cost>
<Health>
<DeathType>vanish</DeathType>
<DeathType>corpse</DeathType>
<Repairable>true</Repairable>
</Health>
<Decay>
<Inactive/>
<DelayTime>0.0</DelayTime>
<SinkRate>4.0</SinkRate>
<SinkAccel>9.8</SinkAccel>
</Decay>
<Armour>
<Hack>10.0</Hack>
<Pierce>42.0</Pierce>

View File

@ -22,6 +22,12 @@
<Max>100</Max>
<Healable>true</Healable>
</Health>
<Decay>
<Inactive/>
<DelayTime>15.0</DelayTime>
<SinkRate>0.05</SinkRate>
<SinkAccel>0.0</SinkAccel>
</Decay>
<Armour>
<Hack>0.0</Hack>
<Pierce>0.0</Pierce>

View File

@ -11,4 +11,10 @@
<Healable>false</Healable>
<Repairable>true</Repairable>
</Health>
<Decay>
<Inactive/>
<DelayTime>0.0</DelayTime>
<SinkRate>0.0</SinkRate>
<SinkAccel>2.0</SinkAccel>
</Decay>
</Entity>

View File

@ -6,8 +6,6 @@
<Position>
<Floating>true</Floating>
</Position>
<Health>
</Health>
<Attack>
<Charge>
<Hack>0.0</Hack>

View File

@ -56,6 +56,9 @@ COMPONENT(UnknownScript)
INTERFACE(CommandQueue)
COMPONENT(CommandQueue)
INTERFACE(Decay)
COMPONENT(Decay)
INTERFACE(Footprint)
COMPONENT(Footprint)

View File

@ -0,0 +1,179 @@
/* Copyright (C) 2010 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#include "precompiled.h"
#include "simulation2/system/Component.h"
#include "ICmpDecay.h"
#include "simulation2/MessageTypes.h"
#include "ICmpPosition.h"
#include "ICmpTerrain.h"
#include "ICmpVisual.h"
/**
* Fairly basic decay implementation, for units and buildings etc.
* The decaying entity remains stationary for some time period, then falls downwards
* with some initial speed and some acceleration, until it has fully sunk.
* The sinking depth is determined from the actor's bounding box and the terrain.
*
* This currently doesn't work with entities whose ICmpPosition has an initial Y offset.
*
* This isn't very efficient (we'll store data and iterate every frame for every entity,
* not just for corpse entities) - it could be designed more optimally if that's a real problem.
*
* Eventually we might want to adjust the decay rate based on user configuration (low-spec
* machines could have fewer corpses), number of corpses, etc.
*
* Must not be used on network-synchronised entities, unless \<Inactive\> is present.
*/
class CCmpDecay : public ICmpDecay
{
public:
static void ClassInit(CComponentManager& componentManager)
{
componentManager.SubscribeToMessageType(MT_Interpolate);
}
DEFAULT_COMPONENT_ALLOCATOR(Decay)
bool m_Active;
float m_DelayTime;
float m_SinkRate;
float m_SinkAccel;
float m_CurrentTime;
float m_TotalSinkDepth; // distance we need to sink (derived from bounding box), or -1 if undetermined
static std::string GetSchema()
{
return
"<element name='DelayTime' a:help='Time to wait before starting to sink, in seconds'>"
"<ref name='nonNegativeDecimal'/>"
"</element>"
"<element name='SinkRate' a:help='Initial rate of sinking, in metres per second'>"
"<ref name='nonNegativeDecimal'/>"
"</element>"
"<element name='SinkAccel' a:help='Acceleration rate of sinking, in metres per second per second'>"
"<ref name='nonNegativeDecimal'/>"
"</element>"
"<optional>"
"<element name='Inactive' a:help='If this element is present, the entity will not do any decaying'>"
"<empty/>"
"</element>"
"</optional>";
}
virtual void Init(const CSimContext& UNUSED(context), const CParamNode& paramNode)
{
m_Active = !paramNode.GetChild("Inactive").IsOk();
m_DelayTime = paramNode.GetChild("DelayTime").ToFixed().ToFloat();
m_SinkRate = paramNode.GetChild("SinkRate").ToFixed().ToFloat();
m_SinkAccel = paramNode.GetChild("SinkAccel").ToFixed().ToFloat();
m_CurrentTime = 0.f;
m_TotalSinkDepth = -1.f;
// Detect unsafe misconfiguration
if (m_Active && !ENTITY_IS_LOCAL(GetEntityId()))
{
debug_warn(L"CCmpDecay must not be used on non-local (network-synchronised) entities");
m_Active = false;
}
}
virtual void Deinit(const CSimContext& UNUSED(context))
{
}
virtual void Serialize(ISerializer& UNUSED(serialize))
{
// This component isn't network-synchronised, so don't serialize anything
}
virtual void Deserialize(const CSimContext& context, const CParamNode& paramNode, IDeserializer& UNUSED(deserialize))
{
Init(context, paramNode);
}
virtual void HandleMessage(const CSimContext& UNUSED(context), const CMessage& msg, bool UNUSED(global))
{
switch (msg.GetType())
{
case MT_Interpolate:
{
if (!m_Active)
break;
const CMessageInterpolate& msgData = static_cast<const CMessageInterpolate&> (msg);
CmpPtr<ICmpPosition> cmpPosition(GetSimContext(), GetEntityId());
if (cmpPosition.null() || !cmpPosition->IsInWorld())
{
// If there's no position (this usually shouldn't happen), destroy the unit immediately
GetSimContext().GetComponentManager().DestroyComponentsSoon(GetEntityId());
break;
}
// Compute the depth the first time this is called
// (This is a bit of an ugly place to do it but at least we'll be sure
// the actor component was loaded already)
if (m_TotalSinkDepth < 0.f)
{
m_TotalSinkDepth = 1.f; // minimum so we always sink at least a little
CmpPtr<ICmpVisual> cmpVisual(GetSimContext(), GetEntityId());
if (!cmpVisual.null())
{
CBound bound = cmpVisual->GetBounds();
m_TotalSinkDepth = std::max(m_TotalSinkDepth, bound[1].Y - bound[0].Y);
}
// If this is a floating unit, we want it to sink all the way under the terrain,
// so find the difference between its current position and the terrain
CFixedVector3D pos = cmpPosition->GetPosition();
CmpPtr<ICmpTerrain> cmpTerrain(GetSimContext(), SYSTEM_ENTITY);
if (!cmpTerrain.null())
{
fixed ground = cmpTerrain->GetGroundLevel(pos.X, pos.Z);
m_TotalSinkDepth += std::max(0.f, (pos.Y - ground).ToFloat());
}
}
m_CurrentTime += msgData.frameTime;
if (m_CurrentTime > m_DelayTime)
{
float t = m_CurrentTime - m_DelayTime;
float depth = (m_SinkRate * t) + (m_SinkAccel * t * t);
cmpPosition->SetHeightOffset(entity_pos_t::FromFloat(-depth));
if (depth > m_TotalSinkDepth)
GetSimContext().GetComponentManager().DestroyComponentsSoon(GetEntityId());
}
break;
}
}
}
};
REGISTER_COMPONENT_TYPE(Decay)

View File

@ -34,6 +34,9 @@
#include "ps/CLogger.h"
#include "renderer/Scene.h"
// Time (in seconds) before projectiles that stuck in the ground are destroyed
const static float PROJECTILE_DECAY_TIME = 60.f;
class CCmpProjectileManager : public ICmpProjectileManager
{
public:
@ -234,7 +237,12 @@ void CCmpProjectileManager::AdvanceProjectile(const CSimContext& context, Projec
// If we're really close to the target now, delete the remaining time so that we don't do a little
// tiny numerically-imprecise movement next frame
if (projectile.timeLeft < 0.01f)
{
projectile.timeLeft = 0;
// Move exactly to the target, so we hit it precisely even if
// the framerate is very low
projectile.pos = projectile.target;
}
// Construct a rotation matrix so that (0,1,0) is in the direction of 'delta'
@ -270,16 +278,26 @@ void CCmpProjectileManager::Interpolate(const CSimContext& context, float frameT
// Remove the ones that have reached their target
for (size_t i = 0; i < m_Projectiles.size(); )
{
// Only remove ones that were hitting entities - leave the ones that hit the ground, because
// it looks pretty. (TODO: they should expire after some limit, not stay forever)
if (m_Projectiles[i].timeLeft <= 0 && m_Projectiles[i].targetEnt != INVALID_ENTITY)
// Projectiles hitting targets get removed immediately.
// Those hitting the ground stay for a while, because it looks pretty.
if (m_Projectiles[i].timeLeft <= 0.f)
{
// Delete in-place by swapping with the last in the list
std::swap(m_Projectiles[i], m_Projectiles.back());
m_Projectiles.pop_back();
if (m_Projectiles[i].targetEnt == INVALID_ENTITY && m_Projectiles[i].timeLeft > -PROJECTILE_DECAY_TIME)
{
// AdvanceProjectile doesn't update timeLeft if the projectile's
// already finished, so just update its timer here
m_Projectiles[i].timeLeft -= frameTime;
}
else
{
// Delete in-place by swapping with the last in the list
std::swap(m_Projectiles[i], m_Projectiles.back());
m_Projectiles.pop_back();
continue; // don't increment i
}
}
else
++i;
++i;
}
}

View File

@ -165,7 +165,7 @@ private:
// Copy the non-interactive components of an entity template (position, actor, etc) into
// a new entity template
void CopyPreviewSubset(CParamNode& out, const CParamNode& in);
void CopyPreviewSubset(CParamNode& out, const CParamNode& in, bool corpse);
// Copy the components of an entity necessary for a construction foundation
// (position, actor, armour, health, etc) into a new entity template
@ -264,7 +264,22 @@ bool CCmpTemplateManager::LoadTemplateFile(const std::string& templateName, int
return false;
}
// Copy a subset to the requested template
CopyPreviewSubset(m_TemplateFileData[templateName], m_TemplateFileData[baseName]);
CopyPreviewSubset(m_TemplateFileData[templateName], m_TemplateFileData[baseName], false);
return true;
}
// Handle special case "corpse|foo"
if (templateName.find("corpse|") == 0)
{
// Load the base entity template, if it wasn't already loaded
std::string baseName = templateName.substr(7);
if (!LoadTemplateFile(baseName, depth+1))
{
LOGERROR(L"Failed to load entity template '%hs'", baseName.c_str());
return false;
}
// Copy a subset to the requested template
CopyPreviewSubset(m_TemplateFileData[templateName], m_TemplateFileData[baseName], true);
return true;
}
@ -390,7 +405,7 @@ std::vector<std::wstring> CCmpTemplateManager::FindAllTemplates()
return templates;
}
void CCmpTemplateManager::CopyPreviewSubset(CParamNode& out, const CParamNode& in)
void CCmpTemplateManager::CopyPreviewSubset(CParamNode& out, const CParamNode& in, bool corpse)
{
// We only want to include components which are necessary (for the visual previewing of an entity)
// and safe (i.e. won't do anything that affects the synchronised simulation state), so additions
@ -401,6 +416,7 @@ void CCmpTemplateManager::CopyPreviewSubset(CParamNode& out, const CParamNode& i
permittedComponentTypes.insert("VisualActor");
permittedComponentTypes.insert("Footprint");
permittedComponentTypes.insert("Obstruction");
permittedComponentTypes.insert("Decay");
// Need these for the Actor Viewer:
permittedComponentTypes.insert("Attack");
@ -416,6 +432,13 @@ void CCmpTemplateManager::CopyPreviewSubset(CParamNode& out, const CParamNode& i
// (but can still be used for testing this entity for collisions against others)
if (out.GetChild("Entity").GetChild("Obstruction").IsOk())
CParamNode::LoadXMLString(out, "<Entity><Obstruction><Inactive/></Obstruction></Entity>");
// Corpses should include decay components and un-inactivate them
if (corpse)
{
if (out.GetChild("Entity").GetChild("Decay").IsOk())
CParamNode::LoadXMLString(out, "<Entity><Decay><Inactive disable=''/></Decay></Entity>");
}
}
void CCmpTemplateManager::CopyFoundationSubset(CParamNode& out, const CParamNode& in)
@ -433,6 +456,7 @@ void CCmpTemplateManager::CopyFoundationSubset(CParamNode& out, const CParamNode
permittedComponentTypes.insert("Footprint");
permittedComponentTypes.insert("Armour");
permittedComponentTypes.insert("Health");
permittedComponentTypes.insert("Decay");
permittedComponentTypes.insert("Cost");
CParamNode::LoadXMLString(out, "<Entity/>");
@ -454,4 +478,3 @@ void CCmpTemplateManager::CopyFoundationSubset(CParamNode& out, const CParamNode
if (out.GetChild("Entity").GetChild("Cost").IsOk())
CParamNode::LoadXMLString(out, "<Entity><Cost><PopulationBonus disable=''/></Cost></Entity>");
}

View File

@ -0,0 +1,25 @@
/* Copyright (C) 2010 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#include "precompiled.h"
#include "ICmpDecay.h"
#include "simulation2/system/InterfaceScripted.h"
BEGIN_INTERFACE_WRAPPER(Decay)
END_INTERFACE_WRAPPER(Decay)

View File

@ -0,0 +1,32 @@
/* Copyright (C) 2010 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef INCLUDED_ICMPDECAY
#define INCLUDED_ICMPDECAY
#include "simulation2/system/Interface.h"
/**
* Animated corpse-decay.
*/
class ICmpDecay : public IComponent
{
public:
DECLARE_INTERFACE_TYPE(Decay)
};
#endif // INCLUDED_ICMPDECAY

View File

@ -47,6 +47,9 @@ public:
* based on entity template "foo" with the non-graphical components removed.
* (This is for previewing construction/placement of units.)
*
* If templateName is of the form "corpse|foo" then it will load a template
* like "preview|foo" but with corpse-related components included.
*
* If templateName is of the form "foundation|foo" then it will load a template
* based on entity template "foo" with various components removed and a few changed
* and added. (This is for constructing foundations of buildings.)