CloudyCraftMinecraft Clone with Real-Time Nether Portals
CloudyCraft is my personal minecraft clone with support for real-time "nether" portals, alternate dimensions,
colored lighting, and stained glass that filters light.
The game was built from the ground up using my own homemade engine, written in C++ and using OpenGL for rendering.
I added real-time portals to the game not only to explore an idea I was passionate about, but also to show what multithreading my chunk generation and I/O enabled me to do.
At the time, I had recently learned about shaders and FBO's (Framebuffer Objects), and I wanted to challenge myself and gain a deeper understanding of what I could do with them.
Players can open a portal to an alternate dimension on any block face.
These are thin portals that can exist on any of the block faces in the game, even thin air!
The gif above shows how to create such a portal, with no frame required like traditional nether portals.
I implemented portals as a bitflag attached to each block, with a bit toggled on or off depending on which of the 6 faces of the block had portals.
This allowed for me to make quick checks to find out which portals were active and inactive by using bitmasking.
Since a lot of checks are made each frame to see whether or not a block has portals, I knew I needed the speed of a bitwise operator to keep the game performant.
The other reason I went for bitflags involved the size of the blocks themselves, as the game currently holds about 1000 chunks in memory at any given time.
I've kept my blocks as flyweight as possible, as there are generally millions of them in memory making every byte add up quickly.
Players are able to pass through any portals to swap dimensions. Entering the dimension in the portal is as simple as walking through the portal (or falling through it!).
As soon as the player's midpoint passes through the middle of a portal, they swap dimensions, and can move freely about the other world.
I wrote the physics for the player using preventative instead of corrective physics (I prevent the player from getting inside of blocks, instead of constantly correcting them whenever they do).
This was one of the hardest parts of the project, as I'd never implemented it before, and it sounded a lot easier than it actually turned out to be.
After a few long sessions, I was able to get it into a good state, where it used small raytraces from the bounds of the box (12 points: the corners and 4 around the box's waist) to determine
whether or not the motion would collide with a block. This actually ended up being the secret to getting my physics to work with crossing portal boundaries, because I was able to
refactor my raycast function to account for portal boundaries, swapping dimensions if it ever needed to while determining collisions.
Portals can exist on any blockface, even the above and below faces of a block.
This allows the player to jump down a portal, adding creative ways to get around the world.
While working on some of the portal physics, I had a bug where if there were blocks behind the portal I'd be unable to pass through, getting stuck inside of
the blocks inside this dimension after starting to move into them. I knew I needed to resolve this issue by giving each of my preventative raycast points a sense of what world they were in.
Before I start my preventative raycasts, I raycast from the player's center out to each of the 12 points on the bounding box, which gives me the world each of those endpoints is in.
This way, when they start the raycast to see if the player can move without hitting anything, each of them checks against the correct world's blocks, allowing players to pass through solid blocks
in one world into open air in another.
I added colored lighting to the game as well.
These colors mix together as they propogate throughout the world, capable of lighting blocks with any 24-bit RGB color.
Colored lights also propogate across portal boundaries as well!
Since the light propogation uses an abstracted version of the blocks to provide seamless transitions across chunk boundaries,
I was able to expand that to support portals. Whenever a block attempts to grab one of it's neighbors,
I check to see if there's a portal on the face that I'm about to cross. If so, we swap dimensions and continue propogation.
Because of the abstract representation of blocks I built, this was a much easier problem to solve than it would appear.
Building Through Portals
When looking through a portal, you can dig and place blocks through the portal into the other world.
If you open up a portal into a wall deep underground, you can dig a little crawl space out through the portal to pass through.
This is one of the most exciting ideas I had about the real-time nether portals when I began designing how they would work.
The idea that if I opened a portal somewhere that was buried underground, I would be able to dig a crawlspace out through the portal to enter the other world.
This works in game because I refactored my Amanatides-Woo Raycast function to account for portals, such that they can cross portal boundaries and begin interacting with the other world.
Since my block digging and placing uses this raycast, adding in support for portals allowed the raycast to begin working across boundaries immediately.
Although it definitely needs some polish to be a gameplay feature that users understand, the concept works just like it should and enables portals to be flexible for users without automatically destroying blocks in the other world.
While working on real-time portals, I wrote a few debugging tools to help me understand the problem space better, as well as to resolve issues with the system itself.
I wrote a simple profiler during the initial development to figure out where I was spending my time each frame, which helped me root out a few problems with my chunk management.
This helped me to prove what parts of my code weren't performant, and helped me to prove when assumptions I was making were valid or invalid, helping me to better understand my codebase.
Along the bottom left of this GIF, you can see a series of extra render targets (You can click on the GIF to make it larger!). The most important one is the 2nd from the left, however.
The 2nd from the left is my portal depth buffer, a second color target that I have attached to the first pass of the shader, where it writes out the depth of any portal values, and writes
the max depth for any geometry that isn't a portal. This target is used in the second pass of the shader, and is how I determine what can be drawn in the portal area. Only things behind
this portal are able to be drawn, and since everything else that's not a portal is at max depth, nothing can be drawn behind it, making this color target function like a combination of stencil and depth buffer.