99297659ee
This commit restores the evaluation order of moving platforms and conveyors to be what it was in 2.2. The evaluation order changed in 2.3 after the patchset to improve the handling of the `obj.entities` and `obj.blocks` vectors (#191). By evaluation order, I'm talking about the order in which platforms and conveyors will be evaluated (and thus will take priority) if Viridian stands on both a conveyor or platform at once, and they either have different speeds or are pointing in different directions. Nowhere in the main game is there a place where you can stand on two different conveyors/platforms at once, so this is solely within the territory of custom levels, which is my specialty. So what caused this evaluation order to change? Well, every moving platform and conveyor in the game is actually made up of two objects: an entity, and a block. The entity is the part that moves around, and the block is the part that actually has the collision. But if the entity is the part that moves around, and entities and blocks are in entirely separate vectors, how is the block part going to move along with it? Well, maybe you'd guess some sort of unique ID system, but spend some time digging around the code and you won't find any trace of any (there's no attribute on an entity to store such an ID, for starters). Instead, what the game does is actually remove all blocks that coincide with the exact top-left corner of the entity, and then create a new one. Destroying and creating blocks like this all the time is hugely wasteful, but hey, it worked. So why did the evaluation order change in 2.3? Well, to understand that, you'll need to understand 2.2's `active` system. Instead of having an object be real simply by virtue of it existing, 2.2 had this system where the object was only real if it had its `active` attribute set to true. In other words, you would be looking at a fake object that didn't actually exist if its `active` attribute was false. On the surface, this doesn't seem that bad. But this can lead to "holes" in a given vector of objects. A hole is simply an inactive object neighbored by active objects (or the inactive object could be the first one in the vector, but then have an active object immediately following it). If you have a vector of 3 objects, all of them active, then removing the second one will result in the vector containing an active object, followed by an inactive object, followed by an active one. However, since the switch to more properly use vectors instead of relying on this `active` system, there's no longer any way for holes to exist in a vector. Properly removing an object from a vector will just shift the rest of the objects down, so if we remove the second object after the vector fix, then this will simply move the third object into the slot of where the second object used to be. So, what happens if you destroy a block and then create a new one in the `active` system? Let's say that your `obj.blocks` looks like this, and here I'm denoting each block by writing out its coordinates: [30,60] [70,90] [80,100] and that you want to update the position of the second one, because the entity that that blocks belongs to has been updated. Okay, so, you delete that block, which then makes things look like this: [30,60] [-] [80,100] and then afterwards, you create a new block with the updated position, resulting in this: [30,60] [74,90] [80,100] Since `entityclass::createblock()` will find the first block slot that has a false `active` attribute, it puts the new object in the same slot as the old one. What has been essentially done here is that the slot of the block has basically been reserved for the new block with the new position. Here, the evaluation order of each block will stay the same. But then 2.3 comes along and changes things up. So we start with an `obj.blocks` like this again: [30,60] [70,90] [80,100] and we want to update the second block, like before. So we remove the second block, resulting in this: [30,60] [80,100] It should be obvious that unlike before, where the third block stayed in the third slot, the third block has now been moved to the second slot. But continuing on; we are now going to create the new block with its updated position, resulting in this: [30,60] [80,100] [70,90] At this point, we can see that the evaluation order of these blocks has been changed due to the fact that the third block has now been moved to the slot that was previously the slot of the second block. So what can we do about this? Well, we can basically emulate what VVVVVV did in 2.2, which is disable a block without actually removing it - except I'm not going to reintroduce an `active` attribute or anything. I'll disable the collision of all blocks at a certain position by setting their widths and heights to 0, and then re-enable them later by finding the first block at that same position, updating its position, and re-assigning its width and height again. The former is what `entityclass::nocollisionat()` does; the latter is what `entityclass::moveblockto()` does. The former mimicks turning off the `active` attribute of all blocks sharing a certain top-left corner; the latter mimicks creating a new block - and it will only do this for one block, because `entityclass::createblock()` in 2.2 only looked for the first block with a false `active` attribute. Now, some quirks relied on the previous behavior of destroying and creating blocks, but all of these quirks have been preserved with the way I implemented this fix. The first quirk is that platforms passing through 0,0 will destroy all spike hitboxes, script boxes, activity zones, and one-way hitboxes in the room. The hitboxes of moving platforms, disappearing platforms, 1x1 quicksand, and conveyors will not be affected. This is a consequence of the fact that the former group uses the `x` and `y` of their `rect`, while the latter group uses the `xp` and `yp` attributes. So the `xp` and `yp` of the former are both 0. Meaning, a platform passing through 0,0 destroys them all. Having these separate coordinates seems like an artifact from the Flash days. (And furthermore, there's an unused `x` and `y` attribute on all blocks, making for technically three separate sets of coordinates! This should probably be cleaned up, except for what I'm about to say...) But actually, if you merge both sets of coordinates into one, this lets moving platforms destroy script boxes and activity zones if it passes through the top-left corner of them, which is probably far worse than the destruction being localized to a specific coordinate that would never likely be reached normally. This quirk is preserved just fine without any special-casing, because instead of destroying all blocks at 0,0, they just get disabled, which does the same job. This quirk seems trivial to fix if I made it so that the position of a platform's block was updated instantaneously instead of having one step to disable it and another step to re-enable it, but I aim to preserve as much quirks as possible. The second quirk is that a moving platform passing through the top-left corner of a disappearing platform or 1x1 quicksand will destroy the block of that disappearing platform. This is because, again, when a moving platform updates, it destroys all blocks at its previous position, not just only one block. This is automatically preserved because this commit just disables the block of the disappearing platform instead of removing it. Just like the last one, this quirk seems extremely trivial to fix, and this time by simply making it so `entityclass::nocollisionat()` would have a `break` statement, i.e. only disabling the first block it finds instead of all blocks it finds, but I want to keep all quirks that are possible to keep. The last quirk is that, apparently, in order to prevent pushing the player vertically out of a moving platform if they get inside of one, the game destroys the block of the moving platform. If I had missed this edge case, then the block would've been destroyed, leaving the moving platform with no collision. But I caught it in my testing, so the block gets disabled instead of destroyed. Also, it seems obtuse for those who don't understand why a platform's block gets destroyed or disabled whenever the player collides with it in `entityclass::collisioncheck()`, so I've put up a comment documenting it as well. The different platform evaluation order desyncs my Nova TAS, but after applying this patchset and #504, my TAS syncs fine (save for the different walkingframe from starting immediately on the ground instead of in the air (#502), but that's minor and can be easily fixed). I've attached a test level to the pull request for this commit (#503) to demonstrate that this patchset not only fixes platform evaluation order, but preserves some bugs and quirks with the existing block system. The first room demonstrates the fixed platform evaluation order, by stepping on the conveyors that both point into each other. In 2.2, Viridian will move to the right of the background pillar, but in 2.3, Viridian will move to the left of the pillar. However, after applying this patch, Viridian will now move to the right of the pillar once again. The second room demonstrates that the platform-passing-through-0,0 trick still works (as explained above). The last room demonstrates that platforms passing through the top-left corners of disappearing platforms or 1x1 quicksand will remove the blocks of those entities, causing Viridian to immediately pass through them. This trick is still preserved after my patchset is applied. |
||
---|---|---|
.. | ||
src | ||
.gitignore | ||
CMakeLists.txt | ||
CONTRIBUTORS.txt | ||
README.md |
How to Build
VVVVVV's official desktop versions are built with the following environments:
- Windows: Visual Studio 2010
- macOS: Xcode CLT, currently targeting 10.9 SDK
- GNU/Linux: CentOS 7
The engine depends solely on SDL2 and SDL2_mixer. All other dependencies are statically linked into the engine. The development libraries for Windows can be downloaded from their respective websites, Linux developers can find the dev libraries from their respective repositories, and macOS developers should compile and install from source (including libogg/libvorbis/libvorbisfile).
Steamworks support is included and the DLL is loaded dynamically, you do not need the SDK headers and there is no special Steam or non-Steam version. The current implementation has been tested with Steamworks SDK v1.46.
To generate the projects on Windows:
# Put your SDL2/SDL2_mixer folders somewhere nice!
mkdir flibitBuild
cd flibitBuild
cmake -G "Visual Studio 10 2010" .. -DSDL2_INCLUDE_DIRS="C:\SDL2-2.0.10\include;C:\SDL2_mixer-2.0.4\include" -DSDL2_LIBRARIES="C:\SDL2-2.0.10\lib\x86\SDL2;C:\SDL2-2.0.10\lib\x86\SDL2main;C:\SDL2_mixer-2.0.4\lib\x86\SDL2_mixer"
Note that on some systems, the SDL2_LIBRARIES
list on Windows may need
SDL2/SDL2main/SDL2_mixer to have .lib
at the end of them. The reason for this
inconsistency is unknown.
To generate everywhere else:
mkdir flibitBuild
cd flibitBuild
cmake ..
macOS may be fussy about the SDK version. How to fix this is up to the whims of however Apple wants to make CMAKE_OSX_SYSROOT annoying to configure and retain each time Xcode updates.
Including data.zip
You'll need the data.zip file from VVVVVV to actually run the game! It's available to download separately for free in the Make and Play edition of the game. Put this file next to your executable and the game should run.
This is intended for personal use only - our license doesn't allow you to actually distribute this data.zip file with your own forks without getting permission from us first. See LICENSE.md for more details. (If you've got a project in mind that requires distributing this file, get in touch!)
A Word About Compiler Quirks
(Note: This section only applies to version 2.2 of the source code, which is the initial commit of this repository. Since then, much hard work has been put in to fix many undefined behaviors. If you're compiling the latest version of the source code, ignore this section.)
This engine is super fussy about optimization levels and runtime checks. In particular, the Windows version absolutely positively must be compiled in Debug mode, with /RTC enabled. If you build in Release mode, or have /RTC disabled, the game behaves dramatically different in ways that were never fully documented (bizarre softlocks, out-of-bounds issues that don't show up in tools like Valgrind, stuff like that). There are lots of things about this old code that could be cleaned up, polished, rewritten, and so on, but this is the one that will probably bite you the hardest when setting up your own build, regardless of platform.
We hope you'll enjoy messing with the source anyway!
Love, flibit