How does vision and the sentry promotion work?
Beware this will contain math. We start our journey in CvPlot.cpp and in there in the method changeAdjacentSight. This method is called on all occasions that change the visibile, fogged and not visible tiles.
So let's start with the simple things in the beginning.
1. We check if the unit, if any, is an air unit. We know those have different vision rules and therefore we remember this in the variable bAerial
2. We save the direction the ground unit is facing, if any.
3. We fill an array with all the different units according to their visibility. We make sure that this array has at least the normal units.
4. If we have a ground unit, city, basically vision from the ground, we increase the iRange by 1. This important so that we see tall tiles like peaks that are normally outside of our vision.
This last one the iRange is as you have already guessed the vision range of the unit and this value is the value increased by 1 with the sentry promotion. Next up the actually interesting part in the code opens up:
As you can see we first iterate through the different invisibility types. Yes the game goes through all the tiles multiple times for each invisibility type. For the standard case though we can ignore this as we are only interested in the general vision type of the majority of units. Next up we open more for loops. Basically we are iterating over all the tiles that could be in vision at all starting from the bottom left going right until we arrive at the top right. So keep in mind that the following code is run over each of those tiles:
As you can see we are doing to bigger if-clauses. As you can see we only go into the first clause if we are processing an air unit or if this omnious shouldProcessDisplacementPlot returns true. I've looked at that method and I'm non the wiser. I spare you the code. Basically what is happening here is that the game calculates something of a vision cone and tiles not in that cone won't be processed, but doing tests on my own this method must return true always ensuring that the well know perfect square of tiles around your unit is processed. If you want to have a go at that method I put it in spoilers:
Inside this first if-clause we first check if the tile in question is in the outermost ring that we added for ground vision and remember this in a variable outerRing. We are almost done with this bigger if-clause. The next part will turn this tile visibile either if the unit is an air unit or if this new method canSeeDisplacementPlot returns true. In this method the actual calculation concering height differences happens and we will have a look at it after we are done here.
We are almost done with this method only looking at the second big if-clause
The developers were merciful here and the comment in there already describes perfectly what is happening here. We ensure that the first ring of tiles is always visible for units.
So the next and last destination on our tour through the vision code is the method canSeeDisplacementPlot and here it is:
A short advise for the parameters of this method. If you remember our previous method then you may know that we put the same x and y values into
int dx and int originalDX/ int dy and int originalDY. We also set bool firstPlot to true.
We start simple again with a basic null reference check for the plot and after that we check if the plot is the tile we are actually standing on. In that case we return true immediately as this tile is always visible. We now turn to some more complicated code, which includes math. If you ever wondered why you need math in those upper math classes in school here is one example for why you need it.
This is a lot of math for something actually very simple to understand. In the end it all boils down to us looking for the shortest path between our tile in question and the tile we are standing on. The little piktogram provided by the developers gives you a good idea of where we are going with this. The one important thing to remember is that you can have multiple shortest path if two tiles have the same distance. You can also see that this method we are looking at is called for each tile on the path again to ensure we can see the tile on the path.
Next we go through the inner part:
So the first two important things are the new terms seeFromLevel and seeThroughLevel. The first one is basically the height of the tile and the second one, which height is required to look through this tile to the next. But beware there are some unintuitive things in their definition. These values are defined in the CIV4TerrainInfos.xml. In there all of grassland, plains, desert, tundra and snow have 1 for both values and coast, ocean, peak and hill all have 0 for both values. Now this sounds strange with hills and peaks having the same value as water tiles. Those two always have one of the other types as a base value, so they are essentially also 1. But this is still not right with peaks and hills having the same values as flatland tiles. Well there is another set of values defined in GlobalDefines.xml containing SEE_FROM_CHANGE and SEE_THROUGH_CHANGE in their name. These are added to the other values. With those in mind we have the following values for seeFromLevel as well as seeThroughLevel:
Watertile = 0
Flatland = 1
Hill = 2
Peak = 3
One special case though are the watertiles. With the right technology (Optics) they get added 1 to their seeFromLevel value.
With those values out of the way let's turn to the code at hand.
First we get the seeFromLevel from the tile we are standing on and then the seeThroughLevel for the tile we are currently checking. We then start first a check for the outerRing tiles:
We get the next tile on the shortest path and keep it in passThroughPlot; we also get its seeThroughLevel. If we are standing higher or on the same level as the passedThrough tile we continue. Next up we return true, meaning the tile can be seen, only if either we are standing higher then the passedThrough tile or if the tile we are checking is higher then our current position. This ensures that only tall structures can be seen in this extra outer ring.
For all the tiles not in this extra outer ring we do the following:
We return true, meaning the tile can be seen, if we are standing higher or on the same level as the tile in question. We also return true if we arrive here with the initial tile being checked, which called this method with firstPlot=true. Now very important this does not mean that every tile in your vision range is turned visible. This is only true for the tile directly adjacent to you. All other tiles rely on the shortest path tiles and if those return false the tile in question is also not visible.
Summary
This consistent of a lot of code so let me try to summarize. First all units have a vision range, most of them just 1 meaning the tiles around them. This range can be increase with the sentry promotion by 1. But this does not mean that the tiles are immediately visible rather it means that all those tiles in your vision range are processed for a vision check. For ground units we also check and additional ring around them for tall structures. Air units are the exception they just reveal all the tiles in their vision.
For the actual vision calculation we reveal all the tiles directly adjacent to you. All tiles farther away calculate the shortest path to your point. If any tiles on that path block your vision the tile in question is not visible. For that all tiles have a height value:
Watertile = 0
Flatland = 1
Hill = 2
Peak = 3
Special care is taken for those outer ring tiles I mentioned. Those can only be visible if not blocked by the shortest path like all other tiles and in addtion are only visible if:
1. Either they are higher then your point.
2. The first tile in your vision ring between them and you has a lower height as you.
Beware this will contain math. We start our journey in CvPlot.cpp and in there in the method changeAdjacentSight. This method is called on all occasions that change the visibile, fogged and not visible tiles.
Code:
void CvPlot::changeAdjacentSight(TeamTypes eTeam, int iRange, bool bIncrement, CvUnit* pUnit, bool bUpdatePlotGroups)
{
bool bAerial = (pUnit != NULL && pUnit->getDomainType() == DOMAIN_AIR);
DirectionTypes eFacingDirection = NO_DIRECTION;
if (!bAerial && NULL != pUnit)
{
eFacingDirection = pUnit->getFacingDirection(true);
}
//fill invisible types
std::vector<InvisibleTypes> aSeeInvisibleTypes;
if (NULL != pUnit)
{
for(int i=0;i<pUnit->getNumSeeInvisibleTypes();i++)
{
aSeeInvisibleTypes.push_back(pUnit->getSeeInvisibleType(i));
}
}
if(aSeeInvisibleTypes.size() == 0)
{
aSeeInvisibleTypes.push_back(NO_INVISIBLE);
}
//check one extra outer ring
if (!bAerial)
{
iRange++;
}
for(int i=0;i<(int)aSeeInvisibleTypes.size();i++)
{
for (int dx = -iRange; dx <= iRange; dx++)
{
for (int dy = -iRange; dy <= iRange; dy++)
{
//check if in facing direction
if (bAerial || shouldProcessDisplacementPlot(dx, dy, iRange - 1, eFacingDirection))
{
bool outerRing = false;
if ((abs(dx) == iRange) || (abs(dy) == iRange))
{
outerRing = true;
}
//check if anything blocking the plot
if (bAerial || canSeeDisplacementPlot(eTeam, dx, dy, dx, dy, true, outerRing))
{
CvPlot* pPlot = plotXY(getX_INLINE(), getY_INLINE(), dx, dy);
if (NULL != pPlot)
{
pPlot->changeVisibilityCount(eTeam, ((bIncrement) ? 1 : -1), aSeeInvisibleTypes[i], bUpdatePlotGroups);
}
}
}
if (eFacingDirection != NO_DIRECTION)
{
if((abs(dx) <= 1) && (abs(dy) <= 1)) //always reveal adjacent plots when using line of sight
{
CvPlot* pPlot = plotXY(getX_INLINE(), getY_INLINE(), dx, dy);
if (NULL != pPlot)
{
pPlot->changeVisibilityCount(eTeam, 1, aSeeInvisibleTypes[i], bUpdatePlotGroups);
pPlot->changeVisibilityCount(eTeam, -1, aSeeInvisibleTypes[i], bUpdatePlotGroups);
}
}
}
}
}
}
}
So let's start with the simple things in the beginning.
1. We check if the unit, if any, is an air unit. We know those have different vision rules and therefore we remember this in the variable bAerial
2. We save the direction the ground unit is facing, if any.
3. We fill an array with all the different units according to their visibility. We make sure that this array has at least the normal units.
4. If we have a ground unit, city, basically vision from the ground, we increase the iRange by 1. This important so that we see tall tiles like peaks that are normally outside of our vision.
This last one the iRange is as you have already guessed the vision range of the unit and this value is the value increased by 1 with the sentry promotion. Next up the actually interesting part in the code opens up:
Code:
for(int i=0;i<(int)aSeeInvisibleTypes.size();i++)
{
for (int dx = -iRange; dx <= iRange; dx++)
{
for (int dy = -iRange; dy <= iRange; dy++)
{
Code:
//check if in facing direction
if (bAerial || shouldProcessDisplacementPlot(dx, dy, iRange - 1, eFacingDirection))
{
bool outerRing = false;
if ((abs(dx) == iRange) || (abs(dy) == iRange))
{
outerRing = true;
}
//check if anything blocking the plot
if (bAerial || canSeeDisplacementPlot(eTeam, dx, dy, dx, dy, true, outerRing))
{
CvPlot* pPlot = plotXY(getX_INLINE(), getY_INLINE(), dx, dy);
if (NULL != pPlot)
{
pPlot->changeVisibilityCount(eTeam, ((bIncrement) ? 1 : -1), aSeeInvisibleTypes[i], bUpdatePlotGroups);
}
}
}
if (eFacingDirection != NO_DIRECTION)
{
if((abs(dx) <= 1) && (abs(dy) <= 1)) //always reveal adjacent plots when using line of sight
{
CvPlot* pPlot = plotXY(getX_INLINE(), getY_INLINE(), dx, dy);
if (NULL != pPlot)
{
pPlot->changeVisibilityCount(eTeam, 1, aSeeInvisibleTypes[i], bUpdatePlotGroups);
pPlot->changeVisibilityCount(eTeam, -1, aSeeInvisibleTypes[i], bUpdatePlotGroups);
}
}
}
As you can see we are doing to bigger if-clauses. As you can see we only go into the first clause if we are processing an air unit or if this omnious shouldProcessDisplacementPlot returns true. I've looked at that method and I'm non the wiser. I spare you the code. Basically what is happening here is that the game calculates something of a vision cone and tiles not in that cone won't be processed, but doing tests on my own this method must return true always ensuring that the well know perfect square of tiles around your unit is processed. If you want to have a go at that method I put it in spoilers:
Inside this first if-clause we first check if the tile in question is in the outermost ring that we added for ground vision and remember this in a variable outerRing. We are almost done with this bigger if-clause. The next part will turn this tile visibile either if the unit is an air unit or if this new method canSeeDisplacementPlot returns true. In this method the actual calculation concering height differences happens and we will have a look at it after we are done here.
We are almost done with this method only looking at the second big if-clause
Code:
if (eFacingDirection != NO_DIRECTION)
{
if((abs(dx) <= 1) && (abs(dy) <= 1)) //always reveal adjacent plots when using line of sight
{
CvPlot* pPlot = plotXY(getX_INLINE(), getY_INLINE(), dx, dy);
if (NULL != pPlot)
{
pPlot->changeVisibilityCount(eTeam, 1, aSeeInvisibleTypes[i], bUpdatePlotGroups);
pPlot->changeVisibilityCount(eTeam, -1, aSeeInvisibleTypes[i], bUpdatePlotGroups);
}
}
}
The developers were merciful here and the comment in there already describes perfectly what is happening here. We ensure that the first ring of tiles is always visible for units.
So the next and last destination on our tour through the vision code is the method canSeeDisplacementPlot and here it is:
Code:
bool CvPlot::canSeeDisplacementPlot(TeamTypes eTeam, int dx, int dy, int originalDX, int originalDY, bool firstPlot, bool outerRing) const
{
CvPlot *pPlot = plotXY(getX_INLINE(), getY_INLINE(), dx, dy);
if (pPlot != NULL)
{
//base case is current plot
if((dx == 0) && (dy == 0))
{
return true;
}
//find closest of three points (1, 2, 3) to original line from Start (S) to End (E)
//The diagonal is computed first as that guarantees a change in position
// -------------
// | | 2 | S |
// -------------
// | E | 1 | 3 |
// -------------
int displacements[3][2] = {{dx - getSign(dx), dy - getSign(dy)}, {dx - getSign(dx), dy}, {dx, dy - getSign(dy)}};
int allClosest[3];
int closest = -1;
for (int i=0;i<3;i++)
{
//int tempClosest = abs(displacements[i][0] * originalDX - displacements[i][1] * originalDY); //more accurate, but less structured on a grid
allClosest[i] = abs(displacements[i][0] * dy - displacements[i][1] * dx); //cross product
if((closest == -1) || (allClosest[i] < closest))
{
closest = allClosest[i];
}
}
//iterate through all minimum plots to see if any of them are passable
for(int i=0;i<3;i++)
{
int nextDX = displacements[i][0];
int nextDY = displacements[i][1];
if((nextDX != dx) || (nextDY != dy)) //make sure we change plots
{
if(allClosest[i] == closest)
{
if(canSeeDisplacementPlot(eTeam, nextDX, nextDY, originalDX, originalDY, false, false))
{
int fromLevel = seeFromLevel(eTeam);
int throughLevel = pPlot->seeThroughLevel();
if(outerRing) //check strictly higher level
{
CvPlot *passThroughPlot = plotXY(getX_INLINE(), getY_INLINE(), nextDX, nextDY);
int passThroughLevel = passThroughPlot->seeThroughLevel();
if (fromLevel >= passThroughLevel)
{
if((fromLevel > passThroughLevel) || (pPlot->seeFromLevel(eTeam) > fromLevel)) //either we can see through to it or it is high enough to see from far
{
return true;
}
}
}
else
{
if(fromLevel >= throughLevel) //we can clearly see this level
{
return true;
}
else if(firstPlot) //we can also see it if it is the first plot that is too tall
{
return true;
}
}
}
}
}
}
}
return false;
}
A short advise for the parameters of this method. If you remember our previous method then you may know that we put the same x and y values into
int dx and int originalDX/ int dy and int originalDY. We also set bool firstPlot to true.
We start simple again with a basic null reference check for the plot and after that we check if the plot is the tile we are actually standing on. In that case we return true immediately as this tile is always visible. We now turn to some more complicated code, which includes math. If you ever wondered why you need math in those upper math classes in school here is one example for why you need it.
Code:
//find closest of three points (1, 2, 3) to original line from Start (S) to End (E)
//The diagonal is computed first as that guarantees a change in position
// -------------
// | | 2 | S |
// -------------
// | E | 1 | 3 |
// -------------
int displacements[3][2] = {{dx - getSign(dx), dy - getSign(dy)}, {dx - getSign(dx), dy}, {dx, dy - getSign(dy)}};
int allClosest[3];
int closest = -1;
for (int i=0;i<3;i++)
{
//int tempClosest = abs(displacements[i][0] * originalDX - displacements[i][1] * originalDY); //more accurate, but less structured on a grid
allClosest[i] = abs(displacements[i][0] * dy - displacements[i][1] * dx); //cross product
if((closest == -1) || (allClosest[i] < closest))
{
closest = allClosest[i];
}
}
//iterate through all minimum plots to see if any of them are passable
for(int i=0;i<3;i++)
{
int nextDX = displacements[i][0];
int nextDY = displacements[i][1];
if((nextDX != dx) || (nextDY != dy)) //make sure we change plots
{
if(allClosest[i] == closest)
{
if(canSeeDisplacementPlot(eTeam, nextDX, nextDY, originalDX, originalDY, false, false))
{
This is a lot of math for something actually very simple to understand. In the end it all boils down to us looking for the shortest path between our tile in question and the tile we are standing on. The little piktogram provided by the developers gives you a good idea of where we are going with this. The one important thing to remember is that you can have multiple shortest path if two tiles have the same distance. You can also see that this method we are looking at is called for each tile on the path again to ensure we can see the tile on the path.
Next we go through the inner part:
Code:
int fromLevel = seeFromLevel(eTeam);
int throughLevel = pPlot->seeThroughLevel();
So the first two important things are the new terms seeFromLevel and seeThroughLevel. The first one is basically the height of the tile and the second one, which height is required to look through this tile to the next. But beware there are some unintuitive things in their definition. These values are defined in the CIV4TerrainInfos.xml. In there all of grassland, plains, desert, tundra and snow have 1 for both values and coast, ocean, peak and hill all have 0 for both values. Now this sounds strange with hills and peaks having the same value as water tiles. Those two always have one of the other types as a base value, so they are essentially also 1. But this is still not right with peaks and hills having the same values as flatland tiles. Well there is another set of values defined in GlobalDefines.xml containing SEE_FROM_CHANGE and SEE_THROUGH_CHANGE in their name. These are added to the other values. With those in mind we have the following values for seeFromLevel as well as seeThroughLevel:
Watertile = 0
Flatland = 1
Hill = 2
Peak = 3
One special case though are the watertiles. With the right technology (Optics) they get added 1 to their seeFromLevel value.
With those values out of the way let's turn to the code at hand.
Code:
int fromLevel = seeFromLevel(eTeam);
int throughLevel = pPlot->seeThroughLevel();
if(outerRing) //check strictly higher level
{
CvPlot *passThroughPlot = plotXY(getX_INLINE(), getY_INLINE(), nextDX, nextDY);
int passThroughLevel = passThroughPlot->seeThroughLevel();
if (fromLevel >= passThroughLevel)
{
if((fromLevel > passThroughLevel) || (pPlot->seeFromLevel(eTeam) > fromLevel)) //either we can see through to it or it is high enough to see from far
{
return true;
}
}
}
else
{
if(fromLevel >= throughLevel) //we can clearly see this level
{
return true;
}
else if(firstPlot) //we can also see it if it is the first plot that is too tall
{
return true;
}
}
First we get the seeFromLevel from the tile we are standing on and then the seeThroughLevel for the tile we are currently checking. We then start first a check for the outerRing tiles:
Code:
CvPlot *passThroughPlot = plotXY(getX_INLINE(), getY_INLINE(), nextDX, nextDY);
int passThroughLevel = passThroughPlot->seeThroughLevel();
if (fromLevel >= passThroughLevel)
{
if((fromLevel > passThroughLevel) || (pPlot->seeFromLevel(eTeam) > fromLevel)) //either we can see through to it or it is high enough to see from far
{
return true;
}
}
We get the next tile on the shortest path and keep it in passThroughPlot; we also get its seeThroughLevel. If we are standing higher or on the same level as the passedThrough tile we continue. Next up we return true, meaning the tile can be seen, only if either we are standing higher then the passedThrough tile or if the tile we are checking is higher then our current position. This ensures that only tall structures can be seen in this extra outer ring.
For all the tiles not in this extra outer ring we do the following:
Code:
if(fromLevel >= throughLevel) //we can clearly see this level
{
return true;
}
else if(firstPlot) //we can also see it if it is the first plot that is too tall
{
return true;
}
We return true, meaning the tile can be seen, if we are standing higher or on the same level as the tile in question. We also return true if we arrive here with the initial tile being checked, which called this method with firstPlot=true. Now very important this does not mean that every tile in your vision range is turned visible. This is only true for the tile directly adjacent to you. All other tiles rely on the shortest path tiles and if those return false the tile in question is also not visible.
Summary
This consistent of a lot of code so let me try to summarize. First all units have a vision range, most of them just 1 meaning the tiles around them. This range can be increase with the sentry promotion by 1. But this does not mean that the tiles are immediately visible rather it means that all those tiles in your vision range are processed for a vision check. For ground units we also check and additional ring around them for tall structures. Air units are the exception they just reveal all the tiles in their vision.
For the actual vision calculation we reveal all the tiles directly adjacent to you. All tiles farther away calculate the shortest path to your point. If any tiles on that path block your vision the tile in question is not visible. For that all tiles have a height value:
Watertile = 0
Flatland = 1
Hill = 2
Peak = 3
Special care is taken for those outer ring tiles I mentioned. Those can only be visible if not blocked by the shortest path like all other tiles and in addtion are only visible if:
1. Either they are higher then your point.
2. The first tile in your vision ring between them and you has a lower height as you.
Mods: RtR CtH
Pitboss: PB39, PB40, PB52, PB59 Useful Collections: Pickmethods, Mapmaking, Curious Civplayer
Buy me a coffee
Pitboss: PB39, PB40, PB52, PB59 Useful Collections: Pickmethods, Mapmaking, Curious Civplayer
Buy me a coffee