fix: canTargetUnit() scanning algorithm #1431
Open
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Overview:
Details
From the current version of the code:
Visualizing canTargetUnit in its current state gives us this picture:
This means that 5 of 16 types of units in the base game are significally shorter than they should be, in terms of visibility and detection, with soldier being the leader - 4 voxels shorter, ~18% of its height. Another 2 units are 2 voxels shorter, and another 4 units are 1 voxel shorter each.
How it looks like (mutons should always be visible): download video
In order to fix that, I split the issue to three parts.
We need to make sure that the difference between max unit height and it's centre is an even number. We scan voxels "in pairs", assuming that if we hit a voxel - there's another one underneath it (with exception for bottom plane of floaters and cyberdisks). In that regard, we need the topmost voxel of units to be "scannable". So, the very first step is to increment centre Z coordinate by 1 in case we can't hit the top voxel while gradually incrementing centre coordinate by 2.
The most glaring issue - we can see that the code misses two offsets: -10 and +10. That's why it just misses tops/bottoms of units entirely. Fix is trivial.
We need to make sure that we have enough scan levels. Logically, number of those levels above the centre should be equal to the one below it - considering some variations between the centre and min/max points. So, just increment levels by 1 if we get odd number of them. For example, cyberdisk on the picture above is totally fine with its centre level but as it has height range of 15, it gets (15/2=7.5 rounded down to) 7 scan levels, and that's 1 level less than needed to scan all its height including topmost voxel.
My suggested fix looks like this:
Vizualisation looks like this:
The next issue is "misses". They are not the problem by themselves, cause they just skip a loop cycle and that's it. The real issue is - according to current code logic, they are used to determine if some voxel lies on top or bottom plane of target's cylinder. Why is that important?
Here's an example (red voxels - current version, ignore the commit pls).
From Volutar's words, it has the following logic. There are three vertical lines. The game ignores front and back lines (red voxels) most of the time, and scans point on the unit's axis. For front/back lines, it scans only their top/bottom points. They are needed to check visibility "in 3D", when you're looking to target at the steep angle from above or below. In that case, all of target's frontal section (including axis) could be hidden behind an obstacle, but the point on the back line could still be visible.
But it never worked the way it was intended.
Game decides to check front/back lines only for "heightRange-1" and "heightRange" indexes, that corresponds to "outermost" height values, which are displayed as "misses" on the first picture. Which effectively turns 3D-Muton into 2D one.
My suggested solution for this issue - directly compare the actural voxel height against unit top/bottom voxels.
Here comes "small bug with a bottom boundary". targetMinHeight doesn't represent its lowest voxel. For non-floating units, standing on Z-level 0 (with zero terrain elevation), targetMinHeight is 0. It's the level of the floor, or ceiling of a lower tile. Checking for unit boundaries should start from targetMinHeight+1.
In my code above, I check agains targetMinHeight+2. If you look at second picture - any lowest scan hits either 1st or 2nd voxel. That's why we should exclude them both.
About "true original X-Com algorithm"
In OG, algorithm was different. It was fixed N-S-E-W cross, with 4 horizontal slices - for top, bottom, 1/4 and 3/4 height. Current implementation was intended to check top and bottom of the unit, like the original one did, but fails to do so. Fixing it (by making it work as intended) will make it closer to OG.
P.S. English is not my language, I haven't had any sleep tonight, and I apologize for possible mistakes.