0ad/source/simulation2/components/CCmpProjectileManager.cpp
bb 157c6af18e Make the space in 0 A.D. non-breaking throughout the codebase.
Avoid cases of filenames
Update years in terms and other legal(ish) documents
Don't update years in license headers, since change is not meaningful

Will add linter rule in seperate commit

Happy recompiling everyone!

Original Patch By: Nescio
Comment By: Gallaecio
Differential Revision: D2620
This was SVN commit r27786.
2023-07-27 20:54:46 +00:00

395 lines
12 KiB
C++
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* Copyright (C) 2022 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, 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, 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);
}
}