mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-16 13:23:56 -07:00
Refs2eac4af3a4, refs56bb858802,91ad17c685,72bd886f80. Differential Revision: https://code.wildfiregames.com/D5110 This was SVN commit r27818.
395 lines
12 KiB
C++
395 lines
12 KiB
C++
/* Copyright (C) 2023 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 "ICmpProjectileManager.h"
|
||
|
||
#include "ICmpObstruction.h"
|
||
#include "ICmpObstructionManager.h"
|
||
#include "ICmpPosition.h"
|
||
#include "ICmpRangeManager.h"
|
||
#include "ICmpTerrain.h"
|
||
#include "simulation2/helpers/Los.h"
|
||
#include "simulation2/MessageTypes.h"
|
||
|
||
#include "graphics/Model.h"
|
||
#include "graphics/Unit.h"
|
||
#include "graphics/UnitManager.h"
|
||
#include "maths/Frustum.h"
|
||
#include "maths/Matrix3D.h"
|
||
#include "maths/Quaternion.h"
|
||
#include "maths/Vector3D.h"
|
||
#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 = 30.f;
|
||
|
||
class CCmpProjectileManager final : public ICmpProjectileManager
|
||
{
|
||
public:
|
||
static void ClassInit(CComponentManager& componentManager)
|
||
{
|
||
componentManager.SubscribeToMessageType(MT_Interpolate);
|
||
componentManager.SubscribeToMessageType(MT_RenderSubmit);
|
||
}
|
||
|
||
DEFAULT_COMPONENT_ALLOCATOR(ProjectileManager)
|
||
|
||
static std::string GetSchema()
|
||
{
|
||
return "<a:component type='system'/><empty/>";
|
||
}
|
||
|
||
void Init(const CParamNode& UNUSED(paramNode)) override
|
||
{
|
||
m_ActorSeed = 0;
|
||
m_NextId = 1;
|
||
}
|
||
|
||
void Deinit() override
|
||
{
|
||
for (size_t i = 0; i < m_Projectiles.size(); ++i)
|
||
GetSimContext().GetUnitManager().DeleteUnit(m_Projectiles[i].unit);
|
||
m_Projectiles.clear();
|
||
}
|
||
|
||
void Serialize(ISerializer& serialize) override
|
||
{
|
||
// Because this is just graphical effects, and because it's all non-deterministic floating point,
|
||
// we don't do much serialization here.
|
||
// (That means projectiles will vanish if you save/load - is that okay?)
|
||
|
||
// The attack code stores the id so that the projectile gets deleted when it hits the target
|
||
serialize.NumberU32_Unbounded("next id", m_NextId);
|
||
}
|
||
|
||
void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) override
|
||
{
|
||
Init(paramNode);
|
||
|
||
// The attack code stores the id so that the projectile gets deleted when it hits the target
|
||
deserialize.NumberU32_Unbounded("next id", m_NextId);
|
||
}
|
||
|
||
void HandleMessage(const CMessage& msg, bool UNUSED(global)) override
|
||
{
|
||
switch (msg.GetType())
|
||
{
|
||
case MT_Interpolate:
|
||
{
|
||
const CMessageInterpolate& msgData = static_cast<const CMessageInterpolate&> (msg);
|
||
Interpolate(msgData.deltaSimTime);
|
||
break;
|
||
}
|
||
case MT_RenderSubmit:
|
||
{
|
||
const CMessageRenderSubmit& msgData = static_cast<const CMessageRenderSubmit&> (msg);
|
||
RenderSubmit(msgData.collector, msgData.frustum, msgData.culling);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
uint32_t LaunchProjectileAtPoint(const CFixedVector3D& launchPoint, const CFixedVector3D& target, fixed speed, fixed gravity, const std::wstring& actorName, const std::wstring& impactActorName, fixed impactAnimationLifetime) override
|
||
{
|
||
return LaunchProjectile(launchPoint, target, speed, gravity, actorName, impactActorName, impactAnimationLifetime);
|
||
}
|
||
|
||
void RemoveProjectile(uint32_t) override;
|
||
|
||
void RenderModel(CModelAbstract& model, const CVector3D& position, SceneCollector& collector, const CFrustum& frustum, bool culling,
|
||
const CLosQuerier& los, bool losRevealAll) const;
|
||
|
||
private:
|
||
struct Projectile
|
||
{
|
||
CUnit* unit;
|
||
CVector3D origin;
|
||
CVector3D pos;
|
||
CVector3D v;
|
||
float time;
|
||
float timeHit;
|
||
float gravity;
|
||
float impactAnimationLifetime;
|
||
uint32_t id;
|
||
std::wstring impactActorName;
|
||
bool isImpactAnimationCreated;
|
||
bool stopped;
|
||
|
||
CVector3D position(float t)
|
||
{
|
||
float t2 = t;
|
||
if (t2 > timeHit)
|
||
t2 = timeHit + logf(1.f + t2 - timeHit);
|
||
|
||
CVector3D ret(origin);
|
||
ret.X += v.X * t2;
|
||
ret.Z += v.Z * t2;
|
||
ret.Y += v.Y * t2 - 0.5f * gravity * t * t;
|
||
return ret;
|
||
}
|
||
};
|
||
|
||
struct ProjectileImpactAnimation
|
||
{
|
||
CUnit* unit;
|
||
CVector3D pos;
|
||
float time;
|
||
};
|
||
|
||
std::vector<Projectile> m_Projectiles;
|
||
|
||
std::vector<ProjectileImpactAnimation> m_ProjectileImpactAnimations;
|
||
|
||
uint32_t m_ActorSeed;
|
||
|
||
uint32_t m_NextId;
|
||
|
||
uint32_t LaunchProjectile(CFixedVector3D launchPoint, CFixedVector3D targetPoint, fixed speed, fixed gravity,
|
||
const std::wstring& actorName, const std::wstring& impactActorName, fixed impactAnimationLifetime);
|
||
|
||
void AdvanceProjectile(Projectile& projectile, float dt) const;
|
||
|
||
void Interpolate(float frameTime);
|
||
|
||
void RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling) const;
|
||
};
|
||
|
||
REGISTER_COMPONENT_TYPE(ProjectileManager)
|
||
|
||
uint32_t CCmpProjectileManager::LaunchProjectile(CFixedVector3D launchPoint, CFixedVector3D targetPoint, fixed speed, fixed gravity, const std::wstring& actorName, const std::wstring& impactActorName, fixed impactAnimationLifetime)
|
||
{
|
||
// This is network synced so don't use GUI checks before incrementing or it breaks any non GUI simulations
|
||
uint32_t currentId = m_NextId++;
|
||
|
||
if (!GetSimContext().HasUnitManager() || actorName.empty())
|
||
return currentId; // do nothing if graphics are disabled
|
||
|
||
Projectile projectile;
|
||
projectile.id = currentId;
|
||
projectile.time = 0.f;
|
||
projectile.stopped = false;
|
||
projectile.gravity = gravity.ToFloat();
|
||
projectile.isImpactAnimationCreated = false;
|
||
|
||
if (!impactActorName.empty())
|
||
{
|
||
projectile.impactActorName = impactActorName;
|
||
projectile.impactAnimationLifetime = impactAnimationLifetime.ToFloat();
|
||
}
|
||
else
|
||
{
|
||
projectile.impactActorName = L"";
|
||
projectile.impactAnimationLifetime = 0.0f;
|
||
}
|
||
|
||
projectile.origin = launchPoint;
|
||
|
||
projectile.unit = GetSimContext().GetUnitManager().CreateUnit(actorName, INVALID_ENTITY, m_ActorSeed++);
|
||
if (!projectile.unit) // The error will have already been logged
|
||
return currentId;
|
||
|
||
projectile.pos = projectile.origin;
|
||
CVector3D offset(targetPoint);
|
||
offset -= projectile.pos;
|
||
float horizDistance = sqrtf(offset.X*offset.X + offset.Z*offset.Z);
|
||
projectile.timeHit = horizDistance / speed.ToFloat();
|
||
projectile.v = offset * (1.f / projectile.timeHit);
|
||
|
||
projectile.v.Y = offset.Y / projectile.timeHit + 0.5f * projectile.gravity * projectile.timeHit;
|
||
|
||
m_Projectiles.push_back(projectile);
|
||
|
||
return projectile.id;
|
||
}
|
||
|
||
void CCmpProjectileManager::AdvanceProjectile(Projectile& projectile, float dt) const
|
||
{
|
||
projectile.time += dt;
|
||
if (projectile.stopped)
|
||
return;
|
||
|
||
CVector3D delta;
|
||
if (dt < 0.1f)
|
||
delta = projectile.pos;
|
||
else // For big dt delta is unprecise
|
||
delta = projectile.position(projectile.time - 0.1f);
|
||
|
||
projectile.pos = projectile.position(projectile.time);
|
||
|
||
delta = projectile.pos - delta;
|
||
|
||
// If we've passed the target position and haven't stopped yet,
|
||
// carry on until we reach solid land
|
||
if (projectile.time >= projectile.timeHit)
|
||
{
|
||
CmpPtr<ICmpTerrain> cmpTerrain(GetSystemEntity());
|
||
if (cmpTerrain)
|
||
{
|
||
float h = cmpTerrain->GetExactGroundLevel(projectile.pos.X, projectile.pos.Z);
|
||
if (projectile.pos.Y < h)
|
||
{
|
||
projectile.pos.Y = h; // stick precisely to the terrain
|
||
projectile.stopped = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Construct a rotation matrix so that (0,1,0) is in the direction of 'delta'
|
||
|
||
CVector3D up(0, 1, 0);
|
||
|
||
delta.Normalize();
|
||
CVector3D axis = up.Cross(delta);
|
||
if (axis.LengthSquared() < 0.0001f)
|
||
axis = CVector3D(1, 0, 0); // if up & delta are almost collinear, rotate around some other arbitrary axis
|
||
else
|
||
axis.Normalize();
|
||
|
||
float angle = acosf(up.Dot(delta));
|
||
|
||
CMatrix3D transform;
|
||
CQuaternion quat;
|
||
quat.FromAxisAngle(axis, angle);
|
||
quat.ToMatrix(transform);
|
||
|
||
// Then apply the translation
|
||
transform.Translate(projectile.pos);
|
||
|
||
// Move the model
|
||
projectile.unit->GetModel().SetTransform(transform);
|
||
}
|
||
|
||
void CCmpProjectileManager::Interpolate(float frameTime)
|
||
{
|
||
for (size_t i = 0; i < m_Projectiles.size(); ++i)
|
||
{
|
||
AdvanceProjectile(m_Projectiles[i], frameTime);
|
||
}
|
||
|
||
// Remove the ones that have reached their target
|
||
for (size_t i = 0; i < m_Projectiles.size(); )
|
||
{
|
||
if (!m_Projectiles[i].stopped)
|
||
{
|
||
++i;
|
||
continue;
|
||
}
|
||
|
||
if (!m_Projectiles[i].impactActorName.empty() && !m_Projectiles[i].isImpactAnimationCreated)
|
||
{
|
||
m_Projectiles[i].isImpactAnimationCreated = true;
|
||
CMatrix3D transform;
|
||
CQuaternion quat;
|
||
quat.ToMatrix(transform);
|
||
transform.Translate(m_Projectiles[i].pos);
|
||
|
||
CUnit* unit = GetSimContext().GetUnitManager().CreateUnit(m_Projectiles[i].impactActorName, INVALID_ENTITY, m_ActorSeed++);
|
||
unit->GetModel().SetTransform(transform);
|
||
|
||
ProjectileImpactAnimation projectileImpactAnimation;
|
||
projectileImpactAnimation.unit = unit;
|
||
projectileImpactAnimation.time = m_Projectiles[i].impactAnimationLifetime;
|
||
projectileImpactAnimation.pos = m_Projectiles[i].pos;
|
||
m_ProjectileImpactAnimations.push_back(projectileImpactAnimation);
|
||
}
|
||
|
||
// Projectiles hitting targets get removed immediately.
|
||
// Those hitting the ground stay for a while, because it looks pretty.
|
||
if (m_Projectiles[i].time - m_Projectiles[i].timeHit > PROJECTILE_DECAY_TIME)
|
||
{
|
||
// Delete in-place by swapping with the last in the list
|
||
std::swap(m_Projectiles[i], m_Projectiles.back());
|
||
GetSimContext().GetUnitManager().DeleteUnit(m_Projectiles.back().unit);
|
||
m_Projectiles.pop_back();
|
||
continue;
|
||
}
|
||
++i;
|
||
}
|
||
|
||
for (size_t i = 0; i < m_ProjectileImpactAnimations.size();)
|
||
{
|
||
if (m_ProjectileImpactAnimations[i].time > 0)
|
||
{
|
||
m_ProjectileImpactAnimations[i].time -= frameTime;
|
||
++i;
|
||
}
|
||
else
|
||
{
|
||
std::swap(m_ProjectileImpactAnimations[i], m_ProjectileImpactAnimations.back());
|
||
GetSimContext().GetUnitManager().DeleteUnit(m_ProjectileImpactAnimations.back().unit);
|
||
m_ProjectileImpactAnimations.pop_back();
|
||
}
|
||
}
|
||
}
|
||
|
||
void CCmpProjectileManager::RemoveProjectile(uint32_t id)
|
||
{
|
||
// Scan through the projectile list looking for one with the correct id to remove
|
||
for (size_t i = 0; i < m_Projectiles.size(); i++)
|
||
{
|
||
if (m_Projectiles[i].id == id)
|
||
{
|
||
// Delete in-place by swapping with the last in the list
|
||
std::swap(m_Projectiles[i], m_Projectiles.back());
|
||
GetSimContext().GetUnitManager().DeleteUnit(m_Projectiles.back().unit);
|
||
m_Projectiles.pop_back();
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
void CCmpProjectileManager::RenderModel(CModelAbstract& model, const CVector3D& position, SceneCollector& collector,
|
||
const CFrustum& frustum, bool culling, const CLosQuerier& los, bool losRevealAll) const
|
||
{
|
||
// Don't display objects outside the visible area
|
||
ssize_t posi = (ssize_t)(0.5f + position.X / LOS_TILE_SIZE);
|
||
ssize_t posj = (ssize_t)(0.5f + position.Z / LOS_TILE_SIZE);
|
||
if (!losRevealAll && !los.IsVisible(posi, posj))
|
||
return;
|
||
|
||
model.ValidatePosition();
|
||
|
||
if (culling && !frustum.IsBoxVisible(model.GetWorldBoundsRec()))
|
||
return;
|
||
|
||
// TODO: do something about LOS (copy from CCmpVisualActor)
|
||
|
||
collector.SubmitRecursive(&model);
|
||
}
|
||
|
||
void CCmpProjectileManager::RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling) const
|
||
{
|
||
CmpPtr<ICmpRangeManager> cmpRangeManager(GetSystemEntity());
|
||
int player = GetSimContext().GetCurrentDisplayedPlayer();
|
||
CLosQuerier los(cmpRangeManager->GetLosQuerier(player));
|
||
bool losRevealAll = cmpRangeManager->GetLosRevealAll(player);
|
||
|
||
for (const Projectile& projectile : m_Projectiles)
|
||
{
|
||
RenderModel(projectile.unit->GetModel(), projectile.pos, collector, frustum, culling, los, losRevealAll);
|
||
}
|
||
|
||
for (const ProjectileImpactAnimation& projectileImpactAnimation : m_ProjectileImpactAnimations)
|
||
{
|
||
RenderModel(projectileImpactAnimation.unit->GetModel(), projectileImpactAnimation.pos,
|
||
collector, frustum, culling, los, losRevealAll);
|
||
}
|
||
}
|