Line data Source code
1 : /* Copyright (C) 2022 Wildfire Games.
2 : * This file is part of 0 A.D.
3 : *
4 : * 0 A.D. is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 2 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * 0 A.D. is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : * GNU General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU General Public License
15 : * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
16 : */
17 :
18 : #include "precompiled.h"
19 :
20 : #include "simulation2/system/Component.h"
21 : #include "ICmpProjectileManager.h"
22 :
23 : #include "ICmpObstruction.h"
24 : #include "ICmpObstructionManager.h"
25 : #include "ICmpPosition.h"
26 : #include "ICmpRangeManager.h"
27 : #include "ICmpTerrain.h"
28 : #include "simulation2/helpers/Los.h"
29 : #include "simulation2/MessageTypes.h"
30 :
31 : #include "graphics/Model.h"
32 : #include "graphics/Unit.h"
33 : #include "graphics/UnitManager.h"
34 : #include "maths/Frustum.h"
35 : #include "maths/Matrix3D.h"
36 : #include "maths/Quaternion.h"
37 : #include "maths/Vector3D.h"
38 : #include "ps/CLogger.h"
39 : #include "renderer/Scene.h"
40 :
41 : // Time (in seconds) before projectiles that stuck in the ground are destroyed
42 : const static float PROJECTILE_DECAY_TIME = 30.f;
43 :
44 9 : class CCmpProjectileManager final : public ICmpProjectileManager
45 : {
46 : public:
47 116 : static void ClassInit(CComponentManager& componentManager)
48 : {
49 116 : componentManager.SubscribeToMessageType(MT_Interpolate);
50 116 : componentManager.SubscribeToMessageType(MT_RenderSubmit);
51 116 : }
52 :
53 6 : DEFAULT_COMPONENT_ALLOCATOR(ProjectileManager)
54 :
55 116 : static std::string GetSchema()
56 : {
57 116 : return "<a:component type='system'/><empty/>";
58 : }
59 :
60 3 : void Init(const CParamNode& UNUSED(paramNode)) override
61 : {
62 3 : m_ActorSeed = 0;
63 3 : m_NextId = 1;
64 3 : }
65 :
66 3 : void Deinit() override
67 : {
68 3 : for (size_t i = 0; i < m_Projectiles.size(); ++i)
69 0 : GetSimContext().GetUnitManager().DeleteUnit(m_Projectiles[i].unit);
70 3 : m_Projectiles.clear();
71 3 : }
72 :
73 0 : void Serialize(ISerializer& serialize) override
74 : {
75 : // Because this is just graphical effects, and because it's all non-deterministic floating point,
76 : // we don't do much serialization here.
77 : // (That means projectiles will vanish if you save/load - is that okay?)
78 :
79 : // The attack code stores the id so that the projectile gets deleted when it hits the target
80 0 : serialize.NumberU32_Unbounded("next id", m_NextId);
81 0 : }
82 :
83 0 : void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) override
84 : {
85 0 : Init(paramNode);
86 :
87 : // The attack code stores the id so that the projectile gets deleted when it hits the target
88 0 : deserialize.NumberU32_Unbounded("next id", m_NextId);
89 0 : }
90 :
91 0 : void HandleMessage(const CMessage& msg, bool UNUSED(global)) override
92 : {
93 0 : switch (msg.GetType())
94 : {
95 0 : case MT_Interpolate:
96 : {
97 0 : const CMessageInterpolate& msgData = static_cast<const CMessageInterpolate&> (msg);
98 0 : Interpolate(msgData.deltaSimTime);
99 0 : break;
100 : }
101 0 : case MT_RenderSubmit:
102 : {
103 0 : const CMessageRenderSubmit& msgData = static_cast<const CMessageRenderSubmit&> (msg);
104 0 : RenderSubmit(msgData.collector, msgData.frustum, msgData.culling);
105 0 : break;
106 : }
107 : }
108 0 : }
109 :
110 0 : uint32_t LaunchProjectileAtPoint(const CFixedVector3D& launchPoint, const CFixedVector3D& target, fixed speed, fixed gravity, const std::wstring& actorName, const std::wstring& impactActorName, fixed impactAnimationLifetime) override
111 : {
112 0 : return LaunchProjectile(launchPoint, target, speed, gravity, actorName, impactActorName, impactAnimationLifetime);
113 : }
114 :
115 : void RemoveProjectile(uint32_t) override;
116 :
117 : void RenderModel(CModelAbstract& model, const CVector3D& position, SceneCollector& collector, const CFrustum& frustum, bool culling,
118 : const CLosQuerier& los, bool losRevealAll) const;
119 :
120 : private:
121 0 : struct Projectile
122 : {
123 : CUnit* unit;
124 : CVector3D origin;
125 : CVector3D pos;
126 : CVector3D v;
127 : float time;
128 : float timeHit;
129 : float gravity;
130 : float impactAnimationLifetime;
131 : uint32_t id;
132 : std::wstring impactActorName;
133 : bool isImpactAnimationCreated;
134 : bool stopped;
135 :
136 0 : CVector3D position(float t)
137 : {
138 0 : float t2 = t;
139 0 : if (t2 > timeHit)
140 0 : t2 = timeHit + logf(1.f + t2 - timeHit);
141 :
142 0 : CVector3D ret(origin);
143 0 : ret.X += v.X * t2;
144 0 : ret.Z += v.Z * t2;
145 0 : ret.Y += v.Y * t2 - 0.5f * gravity * t * t;
146 0 : return ret;
147 : }
148 : };
149 :
150 0 : struct ProjectileImpactAnimation
151 : {
152 : CUnit* unit;
153 : CVector3D pos;
154 : float time;
155 : };
156 :
157 : std::vector<Projectile> m_Projectiles;
158 :
159 : std::vector<ProjectileImpactAnimation> m_ProjectileImpactAnimations;
160 :
161 : uint32_t m_ActorSeed;
162 :
163 : uint32_t m_NextId;
164 :
165 : uint32_t LaunchProjectile(CFixedVector3D launchPoint, CFixedVector3D targetPoint, fixed speed, fixed gravity,
166 : const std::wstring& actorName, const std::wstring& impactActorName, fixed impactAnimationLifetime);
167 :
168 : void AdvanceProjectile(Projectile& projectile, float dt) const;
169 :
170 : void Interpolate(float frameTime);
171 :
172 : void RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling) const;
173 : };
174 :
175 116 : REGISTER_COMPONENT_TYPE(ProjectileManager)
176 :
177 0 : uint32_t CCmpProjectileManager::LaunchProjectile(CFixedVector3D launchPoint, CFixedVector3D targetPoint, fixed speed, fixed gravity, const std::wstring& actorName, const std::wstring& impactActorName, fixed impactAnimationLifetime)
178 : {
179 : // This is network synced so don't use GUI checks before incrementing or it breaks any non GUI simulations
180 0 : uint32_t currentId = m_NextId++;
181 :
182 0 : if (!GetSimContext().HasUnitManager() || actorName.empty())
183 0 : return currentId; // do nothing if graphics are disabled
184 :
185 0 : Projectile projectile;
186 0 : projectile.id = currentId;
187 0 : projectile.time = 0.f;
188 0 : projectile.stopped = false;
189 0 : projectile.gravity = gravity.ToFloat();
190 0 : projectile.isImpactAnimationCreated = false;
191 :
192 0 : if (!impactActorName.empty())
193 : {
194 0 : projectile.impactActorName = impactActorName;
195 0 : projectile.impactAnimationLifetime = impactAnimationLifetime.ToFloat();
196 : }
197 : else
198 : {
199 0 : projectile.impactActorName = L"";
200 0 : projectile.impactAnimationLifetime = 0.0f;
201 : }
202 :
203 0 : projectile.origin = launchPoint;
204 :
205 0 : projectile.unit = GetSimContext().GetUnitManager().CreateUnit(actorName, m_ActorSeed++);
206 0 : if (!projectile.unit) // The error will have already been logged
207 0 : return currentId;
208 :
209 0 : projectile.pos = projectile.origin;
210 0 : CVector3D offset(targetPoint);
211 0 : offset -= projectile.pos;
212 0 : float horizDistance = sqrtf(offset.X*offset.X + offset.Z*offset.Z);
213 0 : projectile.timeHit = horizDistance / speed.ToFloat();
214 0 : projectile.v = offset * (1.f / projectile.timeHit);
215 :
216 0 : projectile.v.Y = offset.Y / projectile.timeHit + 0.5f * projectile.gravity * projectile.timeHit;
217 :
218 0 : m_Projectiles.push_back(projectile);
219 :
220 0 : return projectile.id;
221 : }
222 :
223 0 : void CCmpProjectileManager::AdvanceProjectile(Projectile& projectile, float dt) const
224 : {
225 0 : projectile.time += dt;
226 0 : if (projectile.stopped)
227 0 : return;
228 :
229 0 : CVector3D delta;
230 0 : if (dt < 0.1f)
231 0 : delta = projectile.pos;
232 : else // For big dt delta is unprecise
233 0 : delta = projectile.position(projectile.time - 0.1f);
234 :
235 0 : projectile.pos = projectile.position(projectile.time);
236 :
237 0 : delta = projectile.pos - delta;
238 :
239 : // If we've passed the target position and haven't stopped yet,
240 : // carry on until we reach solid land
241 0 : if (projectile.time >= projectile.timeHit)
242 : {
243 0 : CmpPtr<ICmpTerrain> cmpTerrain(GetSystemEntity());
244 0 : if (cmpTerrain)
245 : {
246 0 : float h = cmpTerrain->GetExactGroundLevel(projectile.pos.X, projectile.pos.Z);
247 0 : if (projectile.pos.Y < h)
248 : {
249 0 : projectile.pos.Y = h; // stick precisely to the terrain
250 0 : projectile.stopped = true;
251 : }
252 : }
253 : }
254 :
255 : // Construct a rotation matrix so that (0,1,0) is in the direction of 'delta'
256 :
257 0 : CVector3D up(0, 1, 0);
258 :
259 0 : delta.Normalize();
260 0 : CVector3D axis = up.Cross(delta);
261 0 : if (axis.LengthSquared() < 0.0001f)
262 0 : axis = CVector3D(1, 0, 0); // if up & delta are almost collinear, rotate around some other arbitrary axis
263 : else
264 0 : axis.Normalize();
265 :
266 0 : float angle = acosf(up.Dot(delta));
267 :
268 0 : CMatrix3D transform;
269 0 : CQuaternion quat;
270 0 : quat.FromAxisAngle(axis, angle);
271 0 : quat.ToMatrix(transform);
272 :
273 : // Then apply the translation
274 0 : transform.Translate(projectile.pos);
275 :
276 : // Move the model
277 0 : projectile.unit->GetModel().SetTransform(transform);
278 : }
279 :
280 0 : void CCmpProjectileManager::Interpolate(float frameTime)
281 : {
282 0 : for (size_t i = 0; i < m_Projectiles.size(); ++i)
283 : {
284 0 : AdvanceProjectile(m_Projectiles[i], frameTime);
285 : }
286 :
287 : // Remove the ones that have reached their target
288 0 : for (size_t i = 0; i < m_Projectiles.size(); )
289 : {
290 0 : if (!m_Projectiles[i].stopped)
291 : {
292 0 : ++i;
293 0 : continue;
294 : }
295 :
296 0 : if (!m_Projectiles[i].impactActorName.empty() && !m_Projectiles[i].isImpactAnimationCreated)
297 : {
298 0 : m_Projectiles[i].isImpactAnimationCreated = true;
299 0 : CMatrix3D transform;
300 0 : CQuaternion quat;
301 0 : quat.ToMatrix(transform);
302 0 : transform.Translate(m_Projectiles[i].pos);
303 :
304 0 : CUnit* unit = GetSimContext().GetUnitManager().CreateUnit(m_Projectiles[i].impactActorName, m_ActorSeed++);
305 0 : unit->GetModel().SetTransform(transform);
306 :
307 0 : ProjectileImpactAnimation projectileImpactAnimation;
308 0 : projectileImpactAnimation.unit = unit;
309 0 : projectileImpactAnimation.time = m_Projectiles[i].impactAnimationLifetime;
310 0 : projectileImpactAnimation.pos = m_Projectiles[i].pos;
311 0 : m_ProjectileImpactAnimations.push_back(projectileImpactAnimation);
312 : }
313 :
314 : // Projectiles hitting targets get removed immediately.
315 : // Those hitting the ground stay for a while, because it looks pretty.
316 0 : if (m_Projectiles[i].time - m_Projectiles[i].timeHit > PROJECTILE_DECAY_TIME)
317 : {
318 : // Delete in-place by swapping with the last in the list
319 0 : std::swap(m_Projectiles[i], m_Projectiles.back());
320 0 : GetSimContext().GetUnitManager().DeleteUnit(m_Projectiles.back().unit);
321 0 : m_Projectiles.pop_back();
322 0 : continue;
323 : }
324 0 : ++i;
325 : }
326 :
327 0 : for (size_t i = 0; i < m_ProjectileImpactAnimations.size();)
328 : {
329 0 : if (m_ProjectileImpactAnimations[i].time > 0)
330 : {
331 0 : m_ProjectileImpactAnimations[i].time -= frameTime;
332 0 : ++i;
333 : }
334 : else
335 : {
336 0 : std::swap(m_ProjectileImpactAnimations[i], m_ProjectileImpactAnimations.back());
337 0 : GetSimContext().GetUnitManager().DeleteUnit(m_ProjectileImpactAnimations.back().unit);
338 0 : m_ProjectileImpactAnimations.pop_back();
339 : }
340 : }
341 0 : }
342 :
343 0 : void CCmpProjectileManager::RemoveProjectile(uint32_t id)
344 : {
345 : // Scan through the projectile list looking for one with the correct id to remove
346 0 : for (size_t i = 0; i < m_Projectiles.size(); i++)
347 : {
348 0 : if (m_Projectiles[i].id == id)
349 : {
350 : // Delete in-place by swapping with the last in the list
351 0 : std::swap(m_Projectiles[i], m_Projectiles.back());
352 0 : GetSimContext().GetUnitManager().DeleteUnit(m_Projectiles.back().unit);
353 0 : m_Projectiles.pop_back();
354 0 : return;
355 : }
356 : }
357 : }
358 :
359 0 : void CCmpProjectileManager::RenderModel(CModelAbstract& model, const CVector3D& position, SceneCollector& collector,
360 : const CFrustum& frustum, bool culling, const CLosQuerier& los, bool losRevealAll) const
361 : {
362 : // Don't display objects outside the visible area
363 0 : ssize_t posi = (ssize_t)(0.5f + position.X / LOS_TILE_SIZE);
364 0 : ssize_t posj = (ssize_t)(0.5f + position.Z / LOS_TILE_SIZE);
365 0 : if (!losRevealAll && !los.IsVisible(posi, posj))
366 0 : return;
367 :
368 0 : model.ValidatePosition();
369 :
370 0 : if (culling && !frustum.IsBoxVisible(model.GetWorldBoundsRec()))
371 0 : return;
372 :
373 : // TODO: do something about LOS (copy from CCmpVisualActor)
374 :
375 0 : collector.SubmitRecursive(&model);
376 : }
377 :
378 0 : void CCmpProjectileManager::RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling) const
379 : {
380 0 : CmpPtr<ICmpRangeManager> cmpRangeManager(GetSystemEntity());
381 0 : int player = GetSimContext().GetCurrentDisplayedPlayer();
382 0 : CLosQuerier los(cmpRangeManager->GetLosQuerier(player));
383 0 : bool losRevealAll = cmpRangeManager->GetLosRevealAll(player);
384 :
385 0 : for (const Projectile& projectile : m_Projectiles)
386 : {
387 0 : RenderModel(projectile.unit->GetModel(), projectile.pos, collector, frustum, culling, los, losRevealAll);
388 : }
389 :
390 0 : for (const ProjectileImpactAnimation& projectileImpactAnimation : m_ProjectileImpactAnimations)
391 : {
392 0 : RenderModel(projectileImpactAnimation.unit->GetModel(), projectileImpactAnimation.pos,
393 : collector, frustum, culling, los, losRevealAll);
394 : }
395 3 : }
|