Fehniix / UnityMinecraft

A C# Unity Minecraft clone under development for the University of Calabria, Virtual Reality course.
4 stars 2 forks source link

My version of yours #1

Open dcmrobin opened 1 year ago

dcmrobin commented 1 year ago

Hi! I discovered your Minecraft clone a long time ago and downloaded it. I've been working on my version ever since, and was wondering if you could check it out! https://github.com/dcmrobin/UnityMinecraft

Fehniix commented 1 year ago

Hello! It looks awesome! Currently re-installing Unity to check it out. πŸ˜„ I have taken a quick peak through the code and it seems very well written and documented (apologies for the Perlin noise generator being completely uncommented! 🀣 Quite definitely rushed through that one), well done!

dcmrobin commented 1 year ago

Thanks so much! I was wondering if you could help me implement the Greedy Meshing algorithm to make the world chunks load faster.

Fehniix commented 1 year ago

Update! I've managed to play it, and it's amazing, well done! One thing of note, to refine the game slightly, is not using a reflective material on dropped items (great job on making them actually visible from a profile!) and increase gravitational acceleration. Other than that, it looks and feels amazing. :)

I'm not particularly familiar with the Greedy Meshing algorithm but I could definitely give it a shot as soon as I'll have sorted out some IRL stuff, yeah!

I'll also add your fork the my repo's README!

dcmrobin commented 1 year ago

Yay! Thanks so much!

dcmrobin commented 1 year ago

Hey, @Fehniix Do you have any idea as to how to implement block behaviors? What I mean by that is sort of like water flowing (I tried to add water but all I got was just blocks of blue (solid) stuff that didn't flow and you could mine it.) or grass spreading (After an interval of time, dirt becomes grass when there's air above it) and that sort of stuff.

Fehniix commented 1 year ago

@dcmrobin of course! I'd be happy to help. :) In Minecraft water flowing follows very specific spreading rules, which you can take a look here: https://minecraft.fandom.com/wiki/Water#Spreading My suggestion is to first implement a block that is able to spread, as a PoC, in a 3x3 area:

Grass Grass Grass
Grass Water Grass
Air Air Air

becomes:

Grass Grass Grass
Grass Water Grass
Water Water Water

And then recalculate the height depending on the distance from the water source block!

As per grass spreading, I would do exactly what Minecraft does as well: implement a random tick update per chunk. Each chunk object gets its own timer that chooses a block within it randomly, which would run every 3/20th of a second (every 3rd tick basically). If the randomly chosen block is a dirt block that's in a 3x5x3 range from a grass block, it becomes a grass block itself.

dcmrobin commented 1 year ago

@dcmrobin of course! I'd be happy to help. :) In Minecraft water flowing follows very specific spreading rules, which you can take a look here: https://minecraft.fandom.com/wiki/Water#Spreading My suggestion is to first implement a block that is able to spread, as a PoC, in a 3x3 area:

Grass Grass Grass Grass Water Grass Air Air Air becomes:

Grass Grass Grass Grass Water Grass Water Water Water And then recalculate the height depending on the distance from the water source block!

As per grass spreading, I would do exactly what Minecraft does as well: implement a random tick update per chunk. Each chunk object gets its own timer that chooses a block within it randomly, which would run every 3/20th of a second (every 3rd tick basically). If the randomly chosen block is a dirt block that's in a 3x5x3 range from a grass block, it becomes a grass block itself.

@Fehniix Yep! That's basically what I've been trying to implement: if any of the spaces (apart from above) around a water block is air, replace the air block with a water block. That part would be easy... But the problem is how to do the actual checking, and how I would physically change the block. The way I'm thinking of how to do it would be to do this: if BaseBlock[blockPosX + 1, blockPosY, blockPosZ] is equal to air, then change the air block that is at the position blockPosX + 1 to water. This would need to be updated all the time. (By the way, this is super cool collaborating with (or just being helped by) the author of the base minecraft clone that I used! Thank you so much for your help!)

Fehniix commented 1 year ago

@dcmrobin Heyo! Sorry for the late reply, lots going on IRL at the moment.

[...] then change the air block that is at the position blockPosX + 1 to water. This would need to be updated all the time.

Agreed, but a pretty major problem would arise at this point: whenever the water source block spreads to neighbouring air blocks, we would want to render that change... and thus redraw the entire chunk. That could be very inefficient.

I'm thinking a good way around this is to physically decouple the two things from each other, such that:

  1. The water source block, when placed, actually triggers a chunk update which redraws the entire chunk,
  2. The "ghost" neighbouring water blocks are not persisted within the chunk object but are rendered separately.

This might be a minor optimization to the problem above, since the water spreading would not trigger a chunk update. Whenever a block is placed where a "ghost water block" currently exists, we would go through the spreading rules and only update "ghost water blocks" that need to be effectively updated.

dcmrobin commented 1 year ago

@Fehniix that's a great idea! And quite possible although I'm not sure where in the code I would do this check.

Fehniix commented 1 year ago

@dcmrobin I see! Let's break it down and build it back up.

The first objective would probably be being able for a block of water to exist in a specific point in space, and should probably create an implementation for it that inherits from BaseBlock. Chunks & terrain are generated and rendered inside the TerrainGenerator.cs, and we could leverage, in particular, the ChunkGenerationCompleted method to kick the "spreading mechanics" for all water blocks encountered along the way.

A neat way could be to keep an array of water block references encountered whilst rendering the chunk and then call a method on each block to kick off the spreading algorithm and second rendering pipeline (maybe create a whole different camera layer for it too!). It might be not too pleasant to the eye for water to be rendered before chunks in my opinion!

dcmrobin commented 1 year ago

@Fehniix ahh right

dcmrobin commented 1 year ago

@Fehniix One other thing, do you know how I would make a save/load system for different worlds just like Minecraft does? I have a feeling it has something to do with storing an array or list of all the chunks that have been loaded in a file, along with all the player stats: inventory, position, etc. The player stats would be the easy bit, but the list of chunks would be another thing. Also, you made it so that all the loaded chunks would be stored in a dictionary, so that when the player went back to that chunk, that chunk would just be pulled out of the dictionary and all would be well. That's great, and it works... but when you build something on the chunk and then walk away from it so that it gets unloaded, then walk back to it so that it loads again, the thing you built isn't there. I think the way to solve this is to update the chunk that's being stored in the dictionary each time a block is placed. Does that sound like the way to do it?

Fehniix commented 1 year ago

@dcmrobin Hello and Merry Christmas! πŸ˜„

As per different worlds, yeah! A seed system could surely be implemented to be able to generate terrain predictably (which would be fed as the initial value for the Perlin noise generator).

The seed would also make it so that all that is needed for the save file, terrain-wise, is exclusively what changed: we could keep track of just the blocks that the user has placed and mined.

As per the second point, yeah! That is most definitely a bug; and your solution should absolutely work, yes!

dcmrobin commented 1 year ago

@Fehniix Merry Christmas! Ah yes! The seed would most definitely do it! So basically, the save file would have the seed in it, and also a list of blocks and their positions in the world! That sounds great. And as for updating the dictionary, the way I think I'm gonna do it is by finding where the chunks get assigned to the dictionary in the code and making it so that it does that again, every time a block is placed!

(Just to say, if I ask a question, I'm absolutely fine if you don't answer for a month or more 😁)

Fehniix commented 1 year ago

@dcmrobin and happy New Year in a couple hours! 😝

I agree on everything, yes, awesome! Were you to have any difficulty with the implementation, let me know and I'll open a PR! πŸ˜„

dcmrobin commented 1 year ago

@Fehniix Great! What's a PR? (edit: ah a PR is a pull request) Oh, btw I can't find where the chunks get assigned to the dictionary...

dcmrobin commented 1 year ago

@Fehniix xD in other words, I am having trouble with the implementation.

Fehniix commented 1 year ago

@dcmrobin Hey! I'm sorry I just moved to Canada and am still figuring the first couple things out. I'll take a look in the next couple days!

Edit: you were referring in particular to where exactly chunks are accessed and modified?

dcmrobin commented 1 year ago

@Fehniix Wow, what a move! Basically, I was referring to the whole re-saving the chunks to the dictionary. (Y'know the problem with the block that gets placed, then the chunk that it has been placed on gets unloaded, and then when the chunk gets loaded again, the block is gone.)

dcmrobin commented 1 year ago

@Fehniix after that problem is fixed, the probable way to go about making a saving and loading system is to somehow transfer that dictionary to a text file, and then once the world is loaded again, use the dictionary that's in the text file.

Fehniix commented 1 year ago

@dcmrobin Hello! Trying to figure out how come blocks don't get updated... I'm thinking this might be reference-related. I'll investigate further as soon as I'll be back from the office.

As per the text file, yes! But saving the whole dictionary would be extremely expensive. I would suggest saving only the ones that differ from seeded terrain generation. I'd give it a shot with JSON first since the time to implement the feature shouldn't be too much, and if too slow go ahead and implement my own format & parser.

dcmrobin commented 1 year ago

@Fehniix Oh wow that's such a good idea! Like, when you place a block and then update that chunk to the dictionary, that chunk that you added to the dictionary gets added to the text file or something!

Fehniix commented 1 year ago

@dcmrobin Yes! You wouldn't want to do this very frequently however, writing to disk is slow and expensive. I would maybe once every 5 minutes or so!

dcmrobin commented 1 year ago

@Fehniix or maybe when you pause the game, an option comes up that says "Save & quit to title" and when you click it then it would right the modified chunks dictionary and the seed to a file?

Fehniix commented 1 year ago

@dcmrobin That would indeed be a good idea as well! Auto-save could be a nice opt-in feature as well, maybe the user themself could decide the auto-save interval.

dcmrobin commented 1 year ago

@Fehniix Yeah!

dcmrobin commented 1 year ago

@Fehniix So cave generation right now is kind of cavernous. Yeah, that kind of sounds wrong since the word "cavernous" comes from caves, but I mean all the caves are absolutely massive. I discovered that if I use GetSimplexFractal and change a couple of numbers I can get the caves I want: small and winding, so that you can actually explore them instead of falling down straight to bedrock. But when I do this, the chunk generation gets messed up. I've attached some screenshots of it. is there any way to fix this while preserving the small windy caves? Screenshot (45) Screenshot (44) Screenshot (43)

Fehniix commented 1 year ago

@dcmrobin oh. That is extremely interesting. Is the issue still current? If not, how did you solve it?

dcmrobin commented 1 year ago

@Fehniix Oh the problem is still there, but only if I change the numbers on the cave gen to make them small and windy. Also, when I tried to make structures like wooden houses appear in a village sort of thing, not all chunks were generated. It's almost as if I'm sort of overloading the terrain gen with too many numbers or something... And I was wondering if you had implemented some sort of performance optimizer that cut out chunks or did something like in the picture to make the performance better.

dcmrobin commented 1 year ago

@Fehniix Do you know how I would implement block rotation? Like when I place a log on the side of a block, the log is facing sideways like this: image And another thing, how would I make slabs, or stairs, and the like? My guess would be using prefabs like you did for the torch, but that just doesn't seem optimal.

dcmrobin commented 1 year ago

@Fehniix Also the more important thing is lighting. Right now, the caves a fully illuminated. This is due to ambient lighting, which just is everywhere. If I turn off ambient lighting, the caves become dark but everywhere else looks horrible. I've decided that I should not use Unity's lighting system, but make my own. This would be the same as Minecraft's lighting algorithm, and look like this: https://www.youtube.com/watch?v=jat3LfTj5zU Here are some pictures of the problem: https://answers.unity.com/questions/1927047/how-to-make-cave-shadows-cancel-out-indirect-light-1.html I hope you can help me somewhat, or explain Minecraft's lighting algorithm to me :D

Fehniix commented 1 year ago

Hey, sorry! Lots of work lately, didn't have much time at all. I'll take some time to reply possibly tonight or tomorrow. Thanks for your patience! :)

dcmrobin commented 1 year ago

It's okay! Thanks so much!

Fehniix commented 1 year ago

Let's go in order!

1. Terrain generation optimization

Unfortunately the terrain generation algorithm was not supposed to efficient. Being a university project for the virtual reality exam (with a couple tweaks this could run in a VR headset indeed!), the focus was solely setting up an environment in which the player could freely move in and interact with; being a huge Minecraft fan myself, I decided to recreate it.

The generation algorithm is greedy and, if I remember correctly, its complexity is as awful as $\mathcal{O}(n^3)$. I'm very confident this can be optimized using various techniques, but a deep-dive & research is most definitely required; I might try to take a look at some papers when I get some spare time.

2. Block rotation implementation

Here you do have a couple options that I can think of right now!

  1. Use a versor, or unit quaternion. See, by the Euler's rotation theorem you know that the rotation of a coordinate system (or point/rigid body for that matter) around a fixed point (which is this case would be the "center" of a block) can be represented with a single rotation $\theta$ about an Euler axis that runs through said point. Thus you can imagine that a rotation can be represented with a unit vector $\vec{w}$ and a scalar $\theta$. The problem with Euler angles to represent a rigid body's rotation in 3D space is gimbal lock, and quaternions come to the rescue. If I remember correctly Unity has many utility methods to handle quaternions and might even be internally based on them. Needless to say this would allow you to rotate the object in many different and non-Minecraft standard ways, which could be pretty fun.
  2. Use a simple vector and point it cardinally! Rotations would be only allowed in $\frac{\pi}{2}$ increments. For example, the two-dimensional $\vec{w} = (w_x, w_y) = (0, \pi)$ vector could represent a block that is facing away from you, if the $(0, 0)$ vector represents a block that is facing towards you.
  3. Use a rotation enum and write encode/decode methods such that you would be able to map enum values to object rotation value, i.e.: .Back $\rightarrow$ $(0, \pi, 0)$ (or $(\pi, 0, 0)$ ).

3. Slabs, stairs, ...

Hmm. I think you would want to either make your own 3D models for such blocks or implement Minecraft's models logic (which is just awesome): https://minecraft.fandom.com/wiki/Tutorials/Models It's extremely complex, but that complexity doesn't necessarily need to directly translate into this project and implement only a basic version of it.

4. Lighting algorithm

I would very highly suggest you look at this video: https://www.youtube.com/watch?v=u85U0wVkN0Y The guy in the video bakes light exactly like you suggest, which is the route I would have taken as well! If we still want to use HDRP, we could use the Indirect Lighting Controller and tweak both the Indirect Diffuse Intensity and global lighting so that shadows aren't as harsh.

dcmrobin commented 1 year ago

Thank you so much for those suggestions! About the block rotation, I don't really see how I would rotate them using quaternions or vectors... since the cubes in the terrain aren't prefabs, but meshes. Is there a way to just paint a face that's usually on top of the block to the side of the block? Or maybe rotating the whole block as you've suggested.

Fehniix commented 1 year ago

Oh! The rotation logic is to be applied prior to mesh stitching; I would render blocks in their "rotated version", so to speak.

I would also think about adding multiple "terrain layers", so to have single entities (say for example trees or crafting tables/furnaces) be detached from terrain generation & mesh stitching, and to leverage parallel processing as well.

dcmrobin commented 1 year ago

Hm. I don't think I've ever done that before...

Fehniix commented 1 year ago

I would suggest starting by looking at the BuildMeshFaces method - this method builds a given block's mesh faces. What you could do is implement rotation logic here: depending on the block's rotation you could swap faces around. From line 150 to line 178 faces are baked as if the block was facing towards you, rotation isn't taken into consideration. You could implement your logic here! Say, for example, if the block were to be rotated 90 degrees along the y-axis, you could swap front with west, west with back, etc.

dcmrobin commented 1 year ago

Yes! That's a great idea! Thanks!

dcmrobin commented 1 year ago

Ah, I found a problem. You see, the BuildMeshFaces method is only called once when loading a chunk into view. The probable candidate for block rotation is the helpful Place() method you implemented for handling when a block gets placed. I've already gotten it to output a debug message saying "A log has been placed" when a log gets placed. The problem is, I can't see a way of making a particular face of a block change when it's placed down, since the only place where block faces get assigned is the Chunk.cs script, and I don't see a way of referencing that from a block script at runtime.

Fehniix commented 1 year ago

Hmm, that's odd. Isn't the BuildMeshFaces method called also when a block is placed or broken? That's the method responsible for building the chunk's mesh, I couldn't think of another way other than a parallel rendering job for re-rending a chunk after some changes were made.

dcmrobin commented 1 year ago

You're absolutely right! I forgot about that. The only thing left to do would be to communicate with the buildmeshfaces method so that when it's called, it checks if the block was placed on a certain face, and if so, rotate it! The problem is, it would probably rotate all blocks in that chunk... Actually, I could just add an if statement to check if the block is a log, and then do the rotating. The method checks if a block's [any direction] face is facing air, and if it is, render that face. How I'm thinking I'd go about this is checking if the block's [any direction] face is facing any block, and if so rotate the top face towards that. I'm not sure how to go about this, since I can only check if the log is facing, say, "dirt". There's no "any" option...

Thank you so much for all your help btw!

Fehniix commented 1 year ago

Hmm, definitely an interesting approach. What I personally do is implement a "rotation" property in the Block class and use that to determine if the current block needs to be rotated or not; this way you would even have to pass anything to the BuildMeshFaces method because all the rotation data would be promptly available within the block objects themselves that are sitting inside the chunk object.

dcmrobin commented 1 year ago

So like there's a property (maybe an enum?) in a block that says what rotation it should have, and in the buildmeshfaces method, it checks that property, and rotates it? But something I don't understand is how would I determine the rotation that the block has, since I can't find a way to check what face the block is adjacent to, in the block's script. The way it might go is like this: in the Place() method in log.cs script, check what face the log is facing, and set the rotation property of the log to the desired rotation. Now, in the BuildMeshFaces() method, check the rotation property of all blocks, and if one of their rotation value is, say, rotated 90 degrees, swap some faces. This should work if I find a way to check what face of a block the log is facing, right? (sorry if I'm wasting your time or not understanding the blindingly obvious)

Fehniix commented 1 year ago

Hi! First and foremost: don't say sorry. My time is most certainly not wasted - I do very much enjoy this and I'm sure you do too. :)

So like there's a property (maybe an enum?) in a block that says what rotation it should have, and in the buildmeshfaces method, it checks that property, and rotates it?

Precisely, yes! There is currently no such enum so you would want to create it yourself. In BuildMeshFaces you would check for the block's stored enum value and rotate the block accordingly.

dcmrobin commented 1 year ago

Yes! The only thing left to do would be some way of detecting which face the block was on, and then changing the enum accordingly - that's the only thing I can't see a way of doing.

dcmrobin commented 1 year ago

Phew! I've added the checks for each orientation (Up, North, East, South, and West) in the buildmeshfaces method! now the only thing left to do is make the block change it's value in the rotation enum when it's placed by checking which block face it's facing, and then it would work!

Fehniix commented 1 year ago

Well done! πŸ˜„ As per the face detection, what I would do is determine which way the player is currently looking at with respect to the global frame of reference with a 45˚ cutoff. If I remember correctly the camera's rotation should be easily accessible - you could use that to determine what cardinal direction the block is facing. Say, for example, the camera's rotation angle is in the 0Β±44.99ΒΊ range: you could say the block would is facing South, and so on and so forth.

dcmrobin commented 1 year ago

So like having the block rotation detection rely on the camera's rotation? That's a good idea... But it would be a hassle to position yourself in the right place in order to get the correct rotation. I was thinking that the block could rely on which face it was being placed on for its rotation.

Fehniix commented 1 year ago

Yeah, agreed, that is also a possibility. Implement either one... or both if you'd like! The second one sounds much easier and more straightforward, definitely try that one first!