/* Copyright (C) 2015 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 .
*/
#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 "ICmpVisual.h"
#include "simulation2/MessageTypes.h"
#include "graphics/Frustum.h"
#include "graphics/Model.h"
#include "graphics/Unit.h"
#include "graphics/UnitManager.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 : 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 "";
}
virtual void Init(const CParamNode& UNUSED(paramNode))
{
m_ActorSeed = 0;
m_NextId = 1;
}
virtual void Deinit()
{
for (size_t i = 0; i < m_Projectiles.size(); ++i)
GetSimContext().GetUnitManager().DeleteUnit(m_Projectiles[i].unit);
m_Projectiles.clear();
}
virtual void Serialize(ISerializer& serialize)
{
// 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);
}
virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize)
{
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);
}
virtual void HandleMessage(const CMessage& msg, bool UNUSED(global))
{
switch (msg.GetType())
{
case MT_Interpolate:
{
const CMessageInterpolate& msgData = static_cast (msg);
Interpolate(msgData.deltaSimTime);
break;
}
case MT_RenderSubmit:
{
const CMessageRenderSubmit& msgData = static_cast (msg);
RenderSubmit(msgData.collector, msgData.frustum, msgData.culling);
break;
}
}
}
virtual uint32_t LaunchProjectileAtPoint(entity_id_t source, CFixedVector3D target, fixed speed, fixed gravity)
{
return LaunchProjectile(source, target, speed, gravity);
}
virtual void RemoveProjectile(uint32_t);
private:
struct Projectile
{
CUnit* unit;
CVector3D origin;
CVector3D pos;
CVector3D v;
float time;
float timeHit;
float gravity;
bool stopped;
uint32_t id;
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;
}
};
std::vector m_Projectiles;
uint32_t m_ActorSeed;
uint32_t m_NextId;
uint32_t LaunchProjectile(entity_id_t source, CFixedVector3D targetPoint, fixed speed, fixed gravity);
void AdvanceProjectile(Projectile& projectile, float dt);
void Interpolate(float frameTime);
void RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling);
};
REGISTER_COMPONENT_TYPE(ProjectileManager)
uint32_t CCmpProjectileManager::LaunchProjectile(entity_id_t source, CFixedVector3D targetPoint, fixed speed, fixed gravity)
{
// 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())
return currentId; // do nothing if graphics are disabled
CmpPtr cmpSourceVisual(GetSimContext(), source);
if (!cmpSourceVisual)
return currentId;
std::wstring name = cmpSourceVisual->GetProjectileActor();
if (name.empty())
{
// If the actor was actually loaded, complain that it doesn't have a projectile
if (!cmpSourceVisual->GetActorShortName().empty())
LOGERROR("Unit with actor '%s' launched a projectile but has no actor on 'projectile' attachpoint", utf8_from_wstring(cmpSourceVisual->GetActorShortName()));
return currentId;
}
Projectile projectile;
projectile.id = currentId;
projectile.time = 0.f;
projectile.stopped = false;
projectile.gravity = gravity.ToFloat();
projectile.origin = cmpSourceVisual->GetProjectileLaunchPoint();
if (!projectile.origin)
{
// If there's no explicit launch point, take a guess based on the entity position
CmpPtr sourcePos(GetSimContext(), source);
if (!sourcePos)
return currentId;
projectile.origin = sourcePos->GetPosition();
projectile.origin.Y += 3.f;
}
std::set selections;
projectile.unit = GetSimContext().GetUnitManager().CreateUnit(name, m_ActorSeed++, selections);
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)
{
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 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(); )
{
// Projectiles hitting targets get removed immediately.
// Those hitting the ground stay for a while, because it looks pretty.
if (m_Projectiles[i].stopped)
{
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; // don't increment i
}
}
++i;
}
}
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::RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling)
{
CmpPtr cmpRangeManager(GetSystemEntity());
int player = GetSimContext().GetCurrentDisplayedPlayer();
ICmpRangeManager::CLosQuerier los (cmpRangeManager->GetLosQuerier(player));
bool losRevealAll = cmpRangeManager->GetLosRevealAll(player);
for (size_t i = 0; i < m_Projectiles.size(); ++i)
{
// Don't display projectiles outside the visible area
ssize_t posi = (ssize_t)(0.5f + m_Projectiles[i].pos.X / TERRAIN_TILE_SIZE);
ssize_t posj = (ssize_t)(0.5f + m_Projectiles[i].pos.Z / TERRAIN_TILE_SIZE);
if (!losRevealAll && !los.IsVisible(posi, posj))
continue;
CModelAbstract& model = m_Projectiles[i].unit->GetModel();
model.ValidatePosition();
if (culling && !frustum.IsBoxVisible(CVector3D(0, 0, 0), model.GetWorldBoundsRec()))
continue;
// TODO: do something about LOS (copy from CCmpVisualActor)
collector.SubmitRecursive(&model);
}
}