Changeset View
Changeset View
Standalone View
Standalone View
source/simulation2/components/CCmpWayPointRenderer.cpp
/* Copyright (C) 2017 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 "CCmpWayPointRenderer.h" | |||||
std::string CCmpWayPointRenderer::GetSchema() | |||||
{ | |||||
return | |||||
"<a:help>Displays a way point marker where units will go to when tasked to move</a:help>" | |||||
"<a:example>" | |||||
"<MarkerTemplate>special/WayPoint</MarkerTemplate>" | |||||
"<LineThickness>0.75</LineThickness>" | |||||
"<LineStartCap>round</LineStartCap>" | |||||
"<LineEndCap>square</LineEndCap>" | |||||
"<LineColour r='20' g='128' b='240'></LineColour>" | |||||
"<LineDashColour r='158' g='11' b='15'></LineDashColour>" | |||||
"<LineCostClass>default</LineCostClass>" | |||||
"<LinePassabilityClass>default</LinePassabilityClass>" | |||||
"</a:example>" | |||||
"<element name='MarkerTemplate' a:help='Template name for the way point marker entity (typically a waypoint flag actor)'>" | |||||
"<text/>" | |||||
"</element>" | |||||
"<element name='LineTexture' a:help='Texture file to use for the way point line'>" | |||||
"<text />" | |||||
"</element>" | |||||
"<element name='LineTextureMask' a:help='Texture mask to indicate where overlay colors are to be applied (see LineColour and LineDashColour)'>" | |||||
"<text />" | |||||
"</element>" | |||||
"<element name='LineThickness' a:help='Thickness of the marker line connecting the entity to the way point marker'>" | |||||
"<data type='decimal'/>" | |||||
"</element>" | |||||
"<element name='LineColour'>" | |||||
"<attribute name='r'>" | |||||
"<data type='integer'><param name='minInclusive'>0</param><param name='maxInclusive'>255</param></data>" | |||||
"</attribute>" | |||||
"<attribute name='g'>" | |||||
"<data type='integer'><param name='minInclusive'>0</param><param name='maxInclusive'>255</param></data>" | |||||
"</attribute>" | |||||
"<attribute name='b'>" | |||||
"<data type='integer'><param name='minInclusive'>0</param><param name='maxInclusive'>255</param></data>" | |||||
"</attribute>" | |||||
"</element>" | |||||
"<element name='LineDashColour'>" | |||||
"<attribute name='r'>" | |||||
"<data type='integer'><param name='minInclusive'>0</param><param name='maxInclusive'>255</param></data>" | |||||
"</attribute>" | |||||
"<attribute name='g'>" | |||||
"<data type='integer'><param name='minInclusive'>0</param><param name='maxInclusive'>255</param></data>" | |||||
"</attribute>" | |||||
"<attribute name='b'>" | |||||
"<data type='integer'><param name='minInclusive'>0</param><param name='maxInclusive'>255</param></data>" | |||||
"</attribute>" | |||||
"</element>" | |||||
"<element name='LineStartCap'>" | |||||
"<choice>" | |||||
"<value a:help='Abrupt line ending; line endings are not closed'>flat</value>" | |||||
"<value a:help='Semi-circular line end cap'>round</value>" | |||||
"<value a:help='Sharp, pointy line end cap'>sharp</value>" | |||||
"<value a:help='Square line end cap'>square</value>" | |||||
"</choice>" | |||||
"</element>" | |||||
"<element name='LineEndCap'>" | |||||
"<choice>" | |||||
"<value a:help='Abrupt line ending; line endings are not closed'>flat</value>" | |||||
"<value a:help='Semi-circular line end cap'>round</value>" | |||||
"<value a:help='Sharp, pointy line end cap'>sharp</value>" | |||||
"<value a:help='Square line end cap'>square</value>" | |||||
"</choice>" | |||||
"</element>" | |||||
"<element name='LinePassabilityClass' a:help='The pathfinder passability class to use for computing the way point marker line path'>" | |||||
"<text />" | |||||
"</element>" | |||||
"<element name='LineCostClass' a:help='The pathfinder cost class to use for computing the way point marker line path'>" | |||||
"<text />" | |||||
"</element>"; | |||||
} | |||||
void CCmpWayPointRenderer::Init(const CParamNode& paramNode) | |||||
{ | |||||
CCmpItineraryPointRenderer::Init(paramNode); | |||||
} | |||||
void CCmpWayPointRenderer::Deinit() | |||||
{ | |||||
CCmpItineraryPointRenderer::Deinit(); | |||||
} | |||||
void CCmpWayPointRenderer::Serialize(ISerializer& UNUSED(serialize)) | |||||
{ | |||||
} | |||||
void CCmpWayPointRenderer::Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) | |||||
{ | |||||
CCmpItineraryPointRenderer::Deserialize(paramNode, deserialize); | |||||
} | |||||
void CCmpWayPointRenderer::AddPosition(CFixedVector2D pos, bool recompute) | |||||
{ | |||||
CCmpItineraryPointRenderer::AddPosition(pos, recompute); | |||||
} | |||||
void CCmpWayPointRenderer::AddPosition_wrapper(const CFixedVector2D& pos) | |||||
{ | |||||
CCmpItineraryPointRenderer::AddPosition_wrapper(pos); | |||||
} | |||||
void CCmpWayPointRenderer::ConstructAllOverlayLines() | |||||
{ | |||||
CCmpItineraryPointRenderer::ConstructAllOverlayLines(); | |||||
} | |||||
void CCmpWayPointRenderer::ConstructOverlayLines(size_t index) | |||||
{ | |||||
// We need to create a new SOverlayTexturedLine every time we want to change the coordinates after having passed it to the | |||||
// renderer, because it does some fancy vertex buffering thing and caches them internally instead of recomputing them on every | |||||
// pass (which is only sensible). | |||||
while (index >= m_TexturedOverlayLines.size()) | |||||
{ | |||||
std::vector<SOverlayTexturedLine> tmp; | |||||
m_TexturedOverlayLines.push_back(tmp); | |||||
} | |||||
std::vector<std::vector<SOverlayTexturedLine> >::iterator iter = m_TexturedOverlayLines.begin(); | |||||
size_t count = index; | |||||
while (count--) | |||||
iter++; | |||||
(*iter).clear(); | |||||
if (m_Path[index].size() < 2) | |||||
return; | |||||
LineCapType dashesLineCapType = SOverlayTexturedLine::LINECAP_ROUND; // line caps to use for the dashed segments (and any other segment's edges that border it) | |||||
for (std::vector<SVisibilitySegment>::const_iterator it = m_VisibilitySegments[index].begin(); it != m_VisibilitySegments[index].end(); ++it) | |||||
{ | |||||
const SVisibilitySegment& segment = (*it); | |||||
if (segment.m_Visible) | |||||
{ | |||||
// does this segment border on the building or rally point flag on either side? | |||||
bool bordersBuilding = (segment.m_EndIndex == m_Path[index].size() - 1); | |||||
bool bordersFlag = (segment.m_StartIndex == 0); | |||||
// construct solid textured overlay line along a subset of the full path points from startPointIdx to endPointIdx | |||||
SOverlayTexturedLine overlayLine; | |||||
overlayLine.m_Thickness = m_LineThickness; | |||||
overlayLine.m_SimContext = &GetSimContext(); | |||||
overlayLine.m_TextureBase = m_Texture; | |||||
overlayLine.m_TextureMask = m_TextureMask; | |||||
overlayLine.m_Color = m_LineColor; | |||||
overlayLine.m_Closed = false; | |||||
// we should take care to only use m_LineXCap for the actual end points at the building and the rally point; any intermediate | |||||
// end points (i.e., that border a dashed segment) should have the dashed cap | |||||
// the path line is actually in reverse order as well, so let's swap out the start and end caps | |||||
overlayLine.m_StartCapType = (bordersFlag ? m_LineEndCapType : dashesLineCapType); | |||||
overlayLine.m_EndCapType = (bordersBuilding ? m_LineStartCapType : dashesLineCapType); | |||||
overlayLine.m_AlwaysVisible = true; | |||||
// push overlay line coordinates | |||||
ENSURE(segment.m_EndIndex > segment.m_StartIndex); | |||||
for (size_t j = segment.m_StartIndex; j <= segment.m_EndIndex; ++j) // end index is inclusive here | |||||
{ | |||||
overlayLine.m_Coords.push_back(m_Path[index][j].X); | |||||
overlayLine.m_Coords.push_back(m_Path[index][j].Y); | |||||
} | |||||
(*iter).push_back(overlayLine); | |||||
} | |||||
else | |||||
{ | |||||
// construct dashed line from startPointIdx to endPointIdx; add textured overlay lines for it to the render list | |||||
std::vector<CVector2D> straightLine; | |||||
straightLine.push_back(m_Path[index][segment.m_StartIndex]); | |||||
straightLine.push_back(m_Path[index][segment.m_EndIndex]); | |||||
// We always want to the dashed line to end at either point with a full dash (i.e. not a cleared space), so that the dashed | |||||
// area is visually obvious. This requires some calculations to see what size we should make the dashes and clears for them | |||||
// to fit exactly. | |||||
float maxDashSize = 3.f; | |||||
float maxClearSize = 3.f; | |||||
float dashSize = maxDashSize; | |||||
float clearSize = maxClearSize; | |||||
float pairDashRatio = (dashSize / (dashSize + clearSize)); // ratio of the dash's length to a (dash + clear) pair's length | |||||
float distance = (m_Path[index][segment.m_StartIndex] - m_Path[index][segment.m_EndIndex]).Length(); // straight-line distance between the points | |||||
// See how many pairs (dash + clear) of unmodified size can fit into the distance. Then check the remaining distance; if it's not exactly | |||||
// a dash size's worth (which it probably won't be), then adjust the dash/clear sizes slightly so that it is. | |||||
int numFitUnmodified = floor(distance / (dashSize + clearSize)); | |||||
float remainderDistance = distance - (numFitUnmodified * (dashSize + clearSize)); | |||||
// Now we want to make remainderDistance equal exactly one dash size (i.e. maxDashSize) by scaling dashSize and clearSize slightly. | |||||
// We have (remainderDistance - maxDashSize) of space to distribute over numFitUnmodified instances of (dashSize + clearSize) to make | |||||
// it fit, so each (dashSize + clearSize) pair needs to adjust its length by (remainderDistance - maxDashSize)/numFitUnmodified | |||||
// (which will be positive or negative accordingly). This number can then be distributed further proportionally among the dash's | |||||
// length and the clear's length. | |||||
// we always want to have at least one dash/clear pair (i.e., "|===| |===|"); also, we need to avoid division by zero below. | |||||
numFitUnmodified = std::max(1, numFitUnmodified); | |||||
float pairwiseLengthDifference = (remainderDistance - maxDashSize) / numFitUnmodified; // can be either positive or negative | |||||
dashSize += pairDashRatio * pairwiseLengthDifference; | |||||
clearSize += (1 - pairDashRatio) * pairwiseLengthDifference; | |||||
// ------------------------------------------------------------------------------------------------ | |||||
SDashedLine dashedLine; | |||||
SimRender::ConstructDashedLine(straightLine, dashedLine, dashSize, clearSize); | |||||
// build overlay lines for dashes | |||||
size_t numDashes = dashedLine.m_StartIndices.size(); | |||||
for (size_t i = 0; i < numDashes; i++) | |||||
{ | |||||
SOverlayTexturedLine dashOverlay; | |||||
dashOverlay.m_Thickness = m_LineThickness; | |||||
dashOverlay.m_SimContext = &GetSimContext(); | |||||
dashOverlay.m_TextureBase = m_Texture; | |||||
dashOverlay.m_TextureMask = m_TextureMask; | |||||
dashOverlay.m_Color = m_LineDashColor; | |||||
dashOverlay.m_Closed = false; | |||||
dashOverlay.m_StartCapType = dashesLineCapType; | |||||
dashOverlay.m_EndCapType = dashesLineCapType; | |||||
dashOverlay.m_AlwaysVisible = true; | |||||
// TODO: maybe adjust the elevation of the dashes to be a little lower, so that it slides underneath the actual path | |||||
size_t dashStartIndex = dashedLine.m_StartIndices[i]; | |||||
size_t dashEndIndex = dashedLine.GetEndIndex(i); | |||||
ENSURE(dashEndIndex > dashStartIndex); | |||||
for (size_t n = dashStartIndex; n < dashEndIndex; n++) | |||||
{ | |||||
dashOverlay.m_Coords.push_back(dashedLine.m_Points[n].X); | |||||
dashOverlay.m_Coords.push_back(dashedLine.m_Points[n].Y); | |||||
} | |||||
(*iter).push_back(dashOverlay); | |||||
} | |||||
} | |||||
} | |||||
//// <DEBUG> ////////////////////////////////////////////// | |||||
if (m_EnableDebugNodeOverlay) | |||||
{ | |||||
while (index >= m_DebugNodeOverlays.size()) | |||||
{ | |||||
std::vector<SOverlayLine> tmp; | |||||
m_DebugNodeOverlays.push_back(tmp); | |||||
} | |||||
for (size_t j = 0; j < m_Path[index].size(); ++j) | |||||
{ | |||||
SOverlayLine overlayLine; | |||||
overlayLine.m_Color = CColor(1.0f, 1.0f, 1.0f, 1.0f); | |||||
overlayLine.m_Thickness = 1; | |||||
SimRender::ConstructCircleOnGround(GetSimContext(), m_Path[index][j].X, m_Path[index][j].Y, 0.075f, overlayLine, true); | |||||
m_DebugNodeOverlays[index].push_back(overlayLine); | |||||
} | |||||
} | |||||
//// </DEBUG> ////////////////////////////////////////////// | |||||
} | |||||
void CCmpWayPointRenderer::FixFootprintWaypoints(std::vector<CVector2D>& coords, CmpPtr<ICmpPosition> cmpPosition, CmpPtr<ICmpFootprint> cmpFootprint) const | |||||
{ | |||||
ENSURE(cmpPosition); | |||||
ENSURE(cmpFootprint); | |||||
// ----------------------------------------------------------------------------------------------------- | |||||
// TODO: nasty fixed/float conversions everywhere | |||||
// grab the shape and dimensions of the footprint | |||||
entity_pos_t footprintSize0, footprintSize1, footprintHeight; | |||||
ICmpFootprint::EShape footprintShape; | |||||
cmpFootprint->GetShape(footprintShape, footprintSize0, footprintSize1, footprintHeight); | |||||
// grab the center of the footprint | |||||
CFixedVector2D center = cmpPosition->GetPosition2D(); | |||||
// ----------------------------------------------------------------------------------------------------- | |||||
switch (footprintShape) | |||||
{ | |||||
case ICmpFootprint::SQUARE: | |||||
{ | |||||
// in this case, footprintSize0 and 1 indicate the size along the X and Z axes, respectively. | |||||
// the building's footprint could be rotated any which way, so let's get the rotation around the Y axis | |||||
// and the rotated unit vectors in the X/Z plane of the shape's footprint | |||||
// (the Footprint itself holds only the outline, the Position holds the orientation) | |||||
fixed s, c; // sine and cosine of the Y axis rotation angle (aka the yaw) | |||||
fixed a = cmpPosition->GetRotation().Y; | |||||
sincos_approx(a, s, c); | |||||
CFixedVector2D u(c, -s); // unit vector along the rotated X axis | |||||
CFixedVector2D v(s, c); // unit vector along the rotated Z axis | |||||
CFixedVector2D halfSize(footprintSize0 / 2, footprintSize1 / 2); | |||||
// starting from the start position, check if any points are within the footprint of the building | |||||
// (this is possible if the pathfinder was started from a point located within the footprint) | |||||
for (int i = (int)(coords.size() - 1); i >= 0; i--) | |||||
{ | |||||
const CVector2D& wp = coords[i]; | |||||
if (Geometry::PointIsInSquare(CFixedVector2D(fixed::FromFloat(wp.X), fixed::FromFloat(wp.Y)) - center, u, v, halfSize)) | |||||
{ | |||||
coords.erase(coords.begin() + i); | |||||
} | |||||
else | |||||
{ | |||||
break; // point no longer inside footprint, from this point on neither will any of the following be | |||||
} | |||||
} | |||||
// add a point right on the edge of the footprint (nearest to the last waypoint) so that it links up nicely with the rest of the path | |||||
CFixedVector2D lastWaypoint(fixed::FromFloat(coords.back().X), fixed::FromFloat(coords.back().Y)); | |||||
CFixedVector2D footprintEdgePoint = Geometry::NearestPointOnSquare(lastWaypoint - center, u, v, halfSize); // relative to the shape origin (center) | |||||
CVector2D footprintEdge((center.X + footprintEdgePoint.X).ToFloat(), (center.Y + footprintEdgePoint.Y).ToFloat()); | |||||
coords.push_back(footprintEdge); | |||||
} | |||||
break; | |||||
case ICmpFootprint::CIRCLE: | |||||
{ | |||||
// in this case, both footprintSize0 and 1 indicate the circle's radius | |||||
for (int i = (int)(coords.size() - 1); i >= 0; i--) | |||||
{ | |||||
const CVector2D& wp = coords[i]; | |||||
fixed pointDistance = (CFixedVector2D(fixed::FromFloat(wp.X), fixed::FromFloat(wp.Y)) - center).Length(); | |||||
if (pointDistance <= footprintSize0) | |||||
{ | |||||
coords.erase(coords.begin() + i); | |||||
} | |||||
else | |||||
{ | |||||
break; // point no longer inside footprint, from this point on neither will any of the following be | |||||
} | |||||
} | |||||
// add a point right on the edge of the footprint so that it links up nicely with the rest of the path | |||||
CVector2D centerVec2D(center.X.ToFloat(), center.Y.ToFloat()); | |||||
CVector2D centerToLast(coords.back() - centerVec2D); | |||||
coords.push_back(centerVec2D + (centerToLast.Normalized() * footprintSize0.ToFloat())); | |||||
} | |||||
break; | |||||
} | |||||
} | |||||
void CCmpWayPointRenderer::GetClosestsEdgePointFrom(CFixedVector2D& result, CFixedVector2D& start, CmpPtr<ICmpPosition> cmpPosition, CmpPtr<ICmpFootprint> cmpFootprint) const | |||||
{ | |||||
ENSURE(cmpPosition); | |||||
ENSURE(cmpFootprint); | |||||
// grab the shape and dimensions of the footprint | |||||
entity_pos_t footprintSize0, footprintSize1, footprintHeight; | |||||
ICmpFootprint::EShape footprintShape; | |||||
cmpFootprint->GetShape(footprintShape, footprintSize0, footprintSize1, footprintHeight); | |||||
// grab the center of the footprint | |||||
CFixedVector2D center = cmpPosition->GetPosition2D(); | |||||
switch (footprintShape) | |||||
{ | |||||
case ICmpFootprint::SQUARE: | |||||
{ | |||||
// in this case, footprintSize0 and 1 indicate the size along the X and Z axes, respectively. | |||||
// the building's footprint could be rotated any which way, so let's get the rotation around the Y axis | |||||
// and the rotated unit vectors in the X/Z plane of the shape's footprint | |||||
// (the Footprint itself holds only the outline, the Position holds the orientation) | |||||
fixed s, c; // sine and cosine of the Y axis rotation angle (aka the yaw) | |||||
fixed a = cmpPosition->GetRotation().Y; | |||||
sincos_approx(a, s, c); | |||||
CFixedVector2D u(c, -s); // unit vector along the rotated X axis | |||||
CFixedVector2D v(s, c); // unit vector along the rotated Z axis | |||||
CFixedVector2D halfSize(footprintSize0 / 2, footprintSize1 / 2); | |||||
CFixedVector2D footprintEdgePoint = Geometry::NearestPointOnSquare(start - center, u, v, halfSize); | |||||
result = center + footprintEdgePoint; | |||||
break; | |||||
} | |||||
case ICmpFootprint::CIRCLE: | |||||
{ | |||||
// in this case, both footprintSize0 and 1 indicate the circle's radius | |||||
// Transform target to the point nearest on the edge. | |||||
CFixedVector2D centerVec2D(center.X, center.Y); | |||||
CFixedVector2D centerToLast(start - centerVec2D); | |||||
centerToLast.Normalize(); | |||||
result = centerVec2D + (centerToLast.Multiply(footprintSize0)); | |||||
break; | |||||
} | |||||
} | |||||
} | |||||
void CCmpWayPointRenderer::GetVisibilitySegments(std::vector<SVisibilitySegment>& out, size_t index) const | |||||
{ | |||||
out.clear(); | |||||
if (m_Path[index].size() < 2) | |||||
return; | |||||
CmpPtr<ICmpRangeManager> cmpRangeMgr(GetSystemEntity()); | |||||
player_id_t currentPlayer = GetSimContext().GetCurrentDisplayedPlayer(); | |||||
CLosQuerier losQuerier(cmpRangeMgr->GetLosQuerier(currentPlayer)); | |||||
// go through the path node list, comparing each node's visibility with the previous one. If it changes, end the current segment and start | |||||
// a new one at the next point. | |||||
bool lastVisible = losQuerier.IsExplored( | |||||
(fixed::FromFloat(m_Path[index][0].X) / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(), | |||||
(fixed::FromFloat(m_Path[index][0].Y) / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest() | |||||
); | |||||
size_t curSegmentStartIndex = 0; // starting node index of the current segment | |||||
for (size_t k = 1; k < m_Path[index].size(); ++k) | |||||
{ | |||||
// grab tile indices for this coord | |||||
int i = (fixed::FromFloat(m_Path[index][k].X) / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); | |||||
int j = (fixed::FromFloat(m_Path[index][k].Y) / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); | |||||
bool nodeVisible = losQuerier.IsExplored(i, j); | |||||
if (nodeVisible != lastVisible) | |||||
{ | |||||
// visibility changed; write out the segment that was just completed and get ready for the new one | |||||
out.push_back(SVisibilitySegment(lastVisible, curSegmentStartIndex, k - 1)); | |||||
//curSegmentStartIndex = k; // new segment starts here | |||||
curSegmentStartIndex = k - 1; | |||||
lastVisible = nodeVisible; | |||||
} | |||||
} | |||||
// terminate the last segment | |||||
out.push_back(SVisibilitySegment(lastVisible, curSegmentStartIndex, m_Path[index].size() - 1)); | |||||
MergeVisibilitySegments(out); | |||||
} | |||||
void CCmpWayPointRenderer::HandleMessage(const CMessage& msg, bool UNUSED(global)) | |||||
{ | |||||
switch (msg.GetType()) | |||||
{ | |||||
case MT_RenderSubmit: | |||||
{ | |||||
PROFILE("RallyPoint::RenderSubmit"); | |||||
if (m_Displayed && IsSet()) | |||||
{ | |||||
const CMessageRenderSubmit& msgData = static_cast<const CMessageRenderSubmit&> (msg); | |||||
RenderSubmit(msgData.collector); | |||||
} | |||||
} | |||||
break; | |||||
case MT_OwnershipChanged: | |||||
{ | |||||
UpdateMarkers(); // update marker variation to new player's civilization | |||||
} | |||||
break; | |||||
case MT_TurnStart: | |||||
{ | |||||
UpdateOverlayLines(); // check for changes to the SoD and update the overlay lines accordingly | |||||
} | |||||
break; | |||||
case MT_Destroy: | |||||
{ | |||||
for (std::vector<entity_id_t>::iterator it = m_MarkerEntityIds.begin(); it != m_MarkerEntityIds.end(); ++it) | |||||
{ | |||||
if (*it != INVALID_ENTITY) | |||||
{ | |||||
GetSimContext().GetComponentManager().DestroyComponentsSoon(*it); | |||||
*it = INVALID_ENTITY; | |||||
} | |||||
} | |||||
} | |||||
break; | |||||
case MT_PositionChanged: | |||||
{ | |||||
// Unlikely to happen in-game, but can occur in atlas | |||||
// Just recompute the path from the entity to the first rally point | |||||
RecomputeItineraryPointPath_wrapper(0); | |||||
} | |||||
break; | |||||
} | |||||
} | |||||
bool CCmpWayPointRenderer::IsSet() const | |||||
{ | |||||
return CCmpItineraryPointRenderer::IsSet(); | |||||
} | |||||
void CCmpWayPointRenderer::RecomputeAllItineraryPointPaths() | |||||
{ | |||||
m_Path.clear(); | |||||
m_VisibilitySegments.clear(); | |||||
m_TexturedOverlayLines.clear(); | |||||
if (!IsSet()) | |||||
return; // no use computing a path if the way point isn't set | |||||
CmpPtr<ICmpPosition> cmpPosition(GetSimContext(), GetEntityId()); | |||||
if (!cmpPosition || !cmpPosition->IsInWorld()) | |||||
return; // no point going on if this entity doesn't have a position or is outside of the world | |||||
// Not used | |||||
CmpPtr<ICmpFootprint> cmpFootprint(GetSimContext(), GetEntityId()); | |||||
CmpPtr<ICmpPathfinder> cmpPathfinder(GetSimContext(), SYSTEM_ENTITY); | |||||
for (size_t i = 0; i < m_ItineraryPoints.size(); ++i) | |||||
{ | |||||
RecomputeItineraryPointPath(i, cmpPosition, cmpFootprint, cmpPathfinder); | |||||
} | |||||
} | |||||
void CCmpWayPointRenderer::RecomputeItineraryPointPath(size_t index, CmpPtr<ICmpPosition>& cmpPosition, CmpPtr<ICmpFootprint>& cmpFootprint, CmpPtr<ICmpPathfinder> cmpPathfinder) | |||||
{ | |||||
while (index >= m_Path.size()) | |||||
{ | |||||
std::vector<CVector2D> tmp; | |||||
m_Path.push_back(tmp); | |||||
} | |||||
m_Path[index].clear(); | |||||
while (index >= m_VisibilitySegments.size()) | |||||
{ | |||||
std::vector<SVisibilitySegment> tmp; | |||||
m_VisibilitySegments.push_back(tmp); | |||||
} | |||||
m_VisibilitySegments[index].clear(); | |||||
// Find a long path to the goal point -- this uses the tile-based pathfinder, which will return a | |||||
// list of waypoints (i.e. a Path) from the goal to the foundation/previous rally point, where each | |||||
// waypoint is centered at a tile. We'll have to do some post-processing on the path to get it smooth. | |||||
Path path; | |||||
std::vector<Waypoint>& waypoints = path.m_Waypoints; | |||||
CFixedVector2D start(cmpPosition->GetPosition2D()); | |||||
Goal goal = { Goal::POINT, m_ItineraryPoints[index].X, m_ItineraryPoints[index].Y }; | |||||
if (index == 0) | |||||
GetClosestsEdgePointFrom(start, m_ItineraryPoints[index], cmpPosition, cmpFootprint); | |||||
else | |||||
{ | |||||
start.X = m_ItineraryPoints[index - 1].X; | |||||
start.Y = m_ItineraryPoints[index - 1].Y; | |||||
} | |||||
cmpPathfinder->ComputePath(start.X, start.Y, goal, cmpPathfinder->GetPassabilityClass(m_LinePassabilityClass), path); | |||||
// Check if we got a path back; if not we probably have two markers less than one tile apart. | |||||
if (path.m_Waypoints.size() < 2) | |||||
{ | |||||
m_Path[index].emplace_back(start.X.ToFloat(), start.Y.ToFloat()); | |||||
m_Path[index].emplace_back(m_ItineraryPoints[index].X.ToFloat(), m_ItineraryPoints[index].Y.ToFloat()); | |||||
return; | |||||
} | |||||
else if (index == 0) | |||||
{ | |||||
// sometimes this ends up not being optimal if you asked for a long path, so improve. | |||||
CFixedVector2D newend(waypoints[waypoints.size() - 2].x, waypoints[waypoints.size() - 2].z); | |||||
GetClosestsEdgePointFrom(newend, newend, cmpPosition, cmpFootprint); | |||||
waypoints.back().x = newend.X; | |||||
waypoints.back().z = newend.Y; | |||||
} | |||||
else | |||||
{ | |||||
// make sure we actually start at the rallypoint because the pathfinder moves us to a usable tile. | |||||
waypoints.back().x = m_ItineraryPoints[index - 1].X; | |||||
waypoints.back().z = m_ItineraryPoints[index - 1].Y; | |||||
} | |||||
// pathfinder makes us go to the nearest passable cell which isn't always what we want | |||||
waypoints[0].x = m_ItineraryPoints[index].X; | |||||
waypoints[0].z = m_ItineraryPoints[index].Y; | |||||
// From here on, we choose to represent the waypoints as CVector2D floats to avoid to have to convert back and forth | |||||
// between fixed-point Waypoint/CFixedVector2D and various other float-based formats used by interpolation and whatnot. | |||||
// Since we'll only be further using these points for rendering purposes, using floats should be fine. | |||||
for (Waypoint& waypoint : waypoints) | |||||
m_Path[index].emplace_back(waypoint.x.ToFloat(), waypoint.z.ToFloat()); | |||||
// add the start position | |||||
// m_Path[index].emplace_back(m_ItineraryPoints[index].X.ToFloat(), m_ItineraryPoints[index].Y.ToFloat()); | |||||
// Post-processing | |||||
// Linearize the path; | |||||
// Pass through the waypoints, averaging each waypoint with its next one except the last one. Because the path | |||||
// goes from the marker to this entity/the previous flag and we want to keep the point at the marker's exact position, | |||||
// loop backwards through the waypoints so that the marker waypoint is maintained. | |||||
// TODO: see if we can do this at the same time as the waypoint -> coord conversion above | |||||
for (size_t i = m_Path[index].size() - 2; i > 0; --i) | |||||
m_Path[index][i] = (m_Path[index][i] + m_Path[index][i - 1]) / 2.0f; | |||||
// if there's a footprint and this path starts from this entity, remove any points returned by the pathfinder that may be on obstructed footprint tiles | |||||
//if (index == 0 && cmpFootprint) | |||||
// FixFootprintWaypoints(m_Path[index], cmpPosition, cmpFootprint); | |||||
// Eliminate some consecutive waypoints that are visible from eachother. Reduce across a maximum distance of approx. 6 tiles | |||||
// (prevents segments that are too long to properly stick to the terrain) | |||||
ReduceSegmentsByVisibility(m_Path[index], 6); | |||||
// Debug overlays | |||||
if (m_EnableDebugNodeOverlay) | |||||
{ | |||||
while (index >= m_DebugNodeOverlays.size()) | |||||
m_DebugNodeOverlays.emplace_back(); | |||||
m_DebugNodeOverlays[index].clear(); | |||||
} | |||||
if (m_EnableDebugNodeOverlay && m_SmoothPath) | |||||
{ | |||||
// Create separate control point overlays so we can differentiate when using smoothing (offset them a little higher from the | |||||
// terrain so we can still see them after the interpolated points are added) | |||||
for (CVector2D& point : m_Path[index]) | |||||
{ | |||||
SOverlayLine overlayLine; | |||||
overlayLine.m_Color = CColor(1.0f, 0.0f, 0.0f, 1.0f); | |||||
overlayLine.m_Thickness = 2; | |||||
SimRender::ConstructSquareOnGround(GetSimContext(), point.X, point.Y, 0.2f, 0.2f, 1.0f, overlayLine, true); | |||||
m_DebugNodeOverlays[index].push_back(overlayLine); | |||||
} | |||||
} | |||||
if (m_SmoothPath) | |||||
// The number of points to interpolate goes hand in hand with the maximum amount of node links allowed to be joined together | |||||
// by the visibility reduction. The more node links that can be joined together, the more interpolated points you need to | |||||
// generate to be able to deal with local terrain height changes. | |||||
SimRender::InterpolatePointsRNS(m_Path[index], false, 0, 4); // no offset, keep line at its exact path | |||||
// find which point is the last visible point before going into the SoD, so we have a point to compare to on the next turn | |||||
GetVisibilitySegments(m_VisibilitySegments[index], index); | |||||
// build overlay lines for the new path | |||||
ConstructOverlayLines(index); | |||||
} | |||||
void CCmpWayPointRenderer::RecomputeItineraryPointPath_wrapper(size_t index) | |||||
{ | |||||
if (!IsSet()) | |||||
return; | |||||
CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle()); | |||||
if (!cmpPosition || !cmpPosition->IsInWorld()) | |||||
return; | |||||
CmpPtr<ICmpFootprint> cmpFootprint(GetEntityHandle()); | |||||
CmpPtr<ICmpPathfinder> cmpPathfinder(GetSystemEntity()); | |||||
RecomputeItineraryPointPath(index, cmpPosition, cmpFootprint, cmpPathfinder); | |||||
} | |||||
void CCmpWayPointRenderer::ReduceSegmentsByVisibility(std::vector<CVector2D>& coords, unsigned maxSegmentLinks, bool floating) const | |||||
{ | |||||
CmpPtr<ICmpPathfinder> cmpPathFinder(GetSystemEntity()); | |||||
CmpPtr<ICmpTerrain> cmpTerrain(GetSystemEntity()); | |||||
CmpPtr<ICmpWaterManager> cmpWaterManager(GetSystemEntity()); | |||||
ENSURE(cmpPathFinder && cmpTerrain && cmpWaterManager); | |||||
if (coords.size() < 3) | |||||
return; | |||||
// The basic idea is this: starting from a base node, keep checking each individual point along the path to see if there's a visible | |||||
// line between it and the base point. If so, keep going, otherwise, make the last visible point the new base node and start the same | |||||
// process from there on until the entire line is checked. The output is the array of base nodes. | |||||
std::vector<CVector2D> newCoords; | |||||
StationaryOnlyObstructionFilter obstructionFilter; | |||||
entity_pos_t lineRadius = fixed::FromFloat(m_LineThickness); | |||||
pass_class_t passabilityClass = cmpPathFinder->GetPassabilityClass(m_LinePassabilityClass); | |||||
newCoords.push_back(coords[0]); // save the first base node | |||||
size_t baseNodeIdx = 0; | |||||
size_t curNodeIdx = 1; | |||||
float baseNodeY; | |||||
entity_pos_t baseNodeX; | |||||
entity_pos_t baseNodeZ; | |||||
// set initial base node coords | |||||
baseNodeX = fixed::FromFloat(coords[baseNodeIdx].X); | |||||
baseNodeZ = fixed::FromFloat(coords[baseNodeIdx].Y); | |||||
baseNodeY = cmpTerrain->GetExactGroundLevel(coords[baseNodeIdx].X, coords[baseNodeIdx].Y); | |||||
if (floating) | |||||
baseNodeY = std::max(baseNodeY, cmpWaterManager->GetExactWaterLevel(coords[baseNodeIdx].X, coords[baseNodeIdx].Y)); | |||||
while (curNodeIdx < coords.size()) | |||||
{ | |||||
ENSURE(curNodeIdx > baseNodeIdx); // this needs to be true at all times, otherwise we're checking visibility between a point and itself | |||||
entity_pos_t curNodeX = fixed::FromFloat(coords[curNodeIdx].X); | |||||
entity_pos_t curNodeZ = fixed::FromFloat(coords[curNodeIdx].Y); | |||||
float curNodeY = cmpTerrain->GetExactGroundLevel(coords[curNodeIdx].X, coords[curNodeIdx].Y); | |||||
if (floating) | |||||
curNodeY = std::max(curNodeY, cmpWaterManager->GetExactWaterLevel(coords[curNodeIdx].X, coords[curNodeIdx].Y)); | |||||
// find out whether curNode is visible from baseNode (careful; this is in 2D only; terrain height differences are ignored!) | |||||
bool curNodeVisible = cmpPathFinder->CheckMovement(obstructionFilter, baseNodeX, baseNodeZ, curNodeX, curNodeZ, lineRadius, passabilityClass); | |||||
// since height differences are ignored by CheckMovement, let's call two points visible from one another only if they're at | |||||
// roughly the same terrain elevation | |||||
curNodeVisible = curNodeVisible && (fabsf(curNodeY - baseNodeY) < 3.f); // TODO: this could probably use some tuning | |||||
if (maxSegmentLinks > 0) | |||||
// max. amount of node-to-node links to be eliminated (unsigned subtraction is valid because curNodeIdx is always > baseNodeIdx) | |||||
curNodeVisible = curNodeVisible && ((curNodeIdx - baseNodeIdx) <= maxSegmentLinks); | |||||
if (!curNodeVisible) | |||||
{ | |||||
// current node is not visible from the base node, so the previous one was the last visible point from baseNode and should | |||||
// hence become the new base node for further iterations. | |||||
// if curNodeIdx is adjacent to the current baseNode (which is possible due to steep height differences, e.g. hills), then | |||||
// we should take care not to stay stuck at the current base node | |||||
if (curNodeIdx > baseNodeIdx + 1) | |||||
{ | |||||
baseNodeIdx = curNodeIdx - 1; | |||||
} | |||||
else | |||||
{ | |||||
// curNodeIdx == baseNodeIdx + 1 | |||||
baseNodeIdx = curNodeIdx; | |||||
curNodeIdx++; // move the next candidate node one forward so that we don't test a point against itself in the next iteration | |||||
} | |||||
newCoords.push_back(coords[baseNodeIdx]); // add new base node to output list | |||||
// update base node coordinates | |||||
baseNodeX = fixed::FromFloat(coords[baseNodeIdx].X); | |||||
baseNodeZ = fixed::FromFloat(coords[baseNodeIdx].Y); | |||||
baseNodeY = cmpTerrain->GetExactGroundLevel(coords[baseNodeIdx].X, coords[baseNodeIdx].Y); | |||||
if (floating) | |||||
baseNodeY = std::max(baseNodeY, cmpWaterManager->GetExactWaterLevel(coords[baseNodeIdx].X, coords[baseNodeIdx].Y)); | |||||
} | |||||
curNodeIdx++; | |||||
} | |||||
// we always need to add the last point back to the array; if e.g. all the points up to the last one are all visible from the current | |||||
// base node, then the loop above just ends and no endpoint is ever added to the list. | |||||
ENSURE(curNodeIdx == coords.size()); | |||||
newCoords.push_back(coords[coords.size() - 1]); | |||||
coords.swap(newCoords); | |||||
} | |||||
void CCmpWayPointRenderer::RenderSubmit(SceneCollector& collector) | |||||
{ | |||||
CCmpItineraryPointRenderer::RenderSubmit(collector); | |||||
} | |||||
void CCmpWayPointRenderer::SetDisplayed(bool displayed) | |||||
{ | |||||
CCmpItineraryPointRenderer::SetDisplayed(displayed); | |||||
} | |||||
void CCmpWayPointRenderer::SetPosition(const CFixedVector2D& pos) | |||||
{ | |||||
CCmpItineraryPointRenderer::SetPosition(pos); | |||||
} | |||||
void CCmpWayPointRenderer::Shift() | |||||
{ | |||||
// Still not sure why size is sometimes <= 0 | |||||
if (!m_ItineraryPoints.empty()) | |||||
m_ItineraryPoints.erase(m_ItineraryPoints.begin()); | |||||
if (!m_MarkerEntityIds.empty()) | |||||
{ | |||||
CmpPtr<ICmpPosition> markerCmpPosition(GetSimContext(), m_MarkerEntityIds[0]); | |||||
if (markerCmpPosition) | |||||
markerCmpPosition->MoveOutOfWorld(); | |||||
m_MarkerEntityIds.erase(m_MarkerEntityIds.begin()); | |||||
} | |||||
if (!m_VisibilitySegments.empty()) | |||||
m_VisibilitySegments.erase(m_VisibilitySegments.begin()); | |||||
if (!m_TexturedOverlayLines.empty()) | |||||
m_TexturedOverlayLines.erase(m_TexturedOverlayLines.begin()); | |||||
if (!m_Path.empty()) | |||||
m_Path.erase(m_Path.begin()); | |||||
UpdateMarkers(); | |||||
} | |||||
void CCmpWayPointRenderer::Stack(const CFixedVector2D& pos) | |||||
{ | |||||
m_ItineraryPoints.insert(m_ItineraryPoints.begin(), pos); | |||||
UpdateMarkers(); | |||||
// TODO don't recompute everything (maybe just shift everything one back? and recompute first only | |||||
RecomputeAllItineraryPointPaths(); | |||||
} | |||||
void CCmpWayPointRenderer::UpdateMarkers() | |||||
{ | |||||
player_id_t previousOwner = m_LastOwner; | |||||
for (size_t i = 0; i < m_ItineraryPoints.size(); ++i) | |||||
{ | |||||
if (i >= m_MarkerEntityIds.size()) | |||||
m_MarkerEntityIds.push_back(INVALID_ENTITY); | |||||
if (m_MarkerEntityIds[i] == INVALID_ENTITY) | |||||
{ | |||||
// no marker exists yet, create one first | |||||
CComponentManager& componentMgr = GetSimContext().GetComponentManager(); | |||||
// allocate a new entity for the marker | |||||
if (!m_MarkerTemplate.empty()) | |||||
{ | |||||
m_MarkerEntityIds[i] = componentMgr.AllocateNewLocalEntity(); | |||||
if (m_MarkerEntityIds[i] != INVALID_ENTITY) | |||||
m_MarkerEntityIds[i] = componentMgr.AddEntity(m_MarkerTemplate, m_MarkerEntityIds[i]); | |||||
} | |||||
} | |||||
// the marker entity should be valid at this point, otherwise something went wrong trying to allocate it | |||||
if (m_MarkerEntityIds[i] == INVALID_ENTITY) | |||||
LOGERROR("Failed to create way point marker entity"); | |||||
CmpPtr<ICmpPosition> markerCmpPosition(GetSimContext(), m_MarkerEntityIds[i]); | |||||
if (markerCmpPosition) | |||||
{ | |||||
if (m_Displayed && IsSet()) | |||||
markerCmpPosition->MoveTo(m_ItineraryPoints[i].X, m_ItineraryPoints[i].Y); | |||||
else | |||||
markerCmpPosition->MoveOutOfWorld(); | |||||
} | |||||
// Set rally point flag selection based on player civilization | |||||
CmpPtr<ICmpOwnership> cmpOwnership(GetEntityHandle()); | |||||
if (!cmpOwnership) | |||||
continue; | |||||
player_id_t ownerId = cmpOwnership->GetOwner(); | |||||
if (ownerId == INVALID_PLAYER || (ownerId == previousOwner && m_LastMarkerCount > i)) | |||||
continue; | |||||
m_LastOwner = ownerId; | |||||
CmpPtr<ICmpPlayerManager> cmpPlayerManager(GetSystemEntity()); | |||||
// cmpPlayerManager should not be null as long as this method is called on-demand instead of at Init() time | |||||
// (we can't rely on component initialization order in Init()) | |||||
if (!cmpPlayerManager) | |||||
continue; | |||||
CmpPtr<ICmpPlayer> cmpPlayer(GetSimContext(), cmpPlayerManager->GetPlayerByID(ownerId)); | |||||
if (!cmpPlayer) | |||||
continue; | |||||
CmpPtr<ICmpVisual> cmpVisualActor(GetSimContext(), m_MarkerEntityIds[i]); | |||||
if (cmpVisualActor) | |||||
cmpVisualActor->SetVariant("civ", CStrW(cmpPlayer->GetCiv()).ToUTF8()); | |||||
} | |||||
m_LastMarkerCount = m_ItineraryPoints.size(); | |||||
} | |||||
void CCmpWayPointRenderer::UpdateMessageSubscriptions() | |||||
{ | |||||
bool needRender = m_Displayed && IsSet(); | |||||
GetSimContext().GetComponentManager().DynamicSubscriptionNonsync(MT_RenderSubmit, this, needRender); | |||||
} | |||||
void CCmpWayPointRenderer::UpdateOverlayLines() | |||||
{ | |||||
CCmpItineraryPointRenderer::UpdateOverlayLines(); | |||||
} |
Wildfire Games · Phabricator