feather-rs / feather

A Minecraft server implementation in Rust
Apache License 2.0
2.63k stars 141 forks source link

Fluid physics #160

Open caelunshun opened 5 years ago

caelunshun commented 5 years ago

Implement moving of water and lava. Waterlogged blocks should also be handled.

This also includes creation of cobblestone/stone/obsidian when lava and water interact.

Defman commented 4 years ago

As an introduction to the project, I would like to implement water physics. I have a few thoughts on how to go about it and would like some feedback.

However, this scanning would be repeated on every fluid spread and a lot of the same blocks would be scanned. I don't know if there's any way to avoid this since fluid spread is delayed and does not happen instantaneously.

When a water block is placed, a new entity is added to the world with a Fluid tag, Position and counter component. A system then query and checks if the counter has been reached and fluid phystics for the block is processed and the entity is removed from the world.

Attached is BFS which locates the nearest air block on a lower y level.

fn bfs(state: &State, start: BlockPosition) -> Vec<BlockPosition> {
    use std::collections::{VecDeque, HashSet};

    let mut visisted: HashSet<BlockPosition> = HashSet::new();
    let mut queue: VecDeque<BlockPosition> = VecDeque::new();

    let mut depth = 0;
    let mut until_next_depth = 1;
    let mut blocks_in_next_depth = 0;

    let mut drain: Vec<BlockPosition> = Vec::new();

    queue.push_back(start);

    while let Some(pos) = queue.pop_front() {
        // Track visited blocks
        if visisted.contains(&pos) {
            continue;
        }
        visisted.insert(pos);

        // If the block below is air, we have reached the depth which contains the first airblock.
        if let Some(Block::Air) = state.block_at(pos + DOWN) {
            drain.push(pos + DOWN);
        }

        // Add all air surrounding air blocks to the queue
        for dir in PLANE {
            if let Some(Block::Air) = state.block_at(pos + *dir) {
                blocks_in_next_depth += 1;
                queue.push_back(pos + *dir);
            }
        }

        until_next_depth -= 1;
        // When until next depth reaches zero we have checked all blocks at this depth 
        if until_next_depth == 0 {
            // If the drain is not empty we should break since we do not want to go deeper
            if !drain.is_empty() {
                break;
            }
            depth += 1;

            // We have reached the max depth
            if depth >= 5 {
                break;
            }
            until_next_depth = blocks_in_next_depth;
            blocks_in_next_depth = 0;
        }
    }
    drain
}
caelunshun commented 4 years ago

@Defman thanks! A few things:

Feel free to contact me with any questions!

Defman commented 4 years ago
Defman commented 4 years ago

One optimization that is possible is to have a map that maps from block position to nearest drain(air block on lower y level). This map is invalidated every tick and will only aid water updates doing that tick and may only speed up the BFS. That is if two water source blocks are placed near each other and are spreading at the same tick it will only calculate the overlapped area once instead of twice.

All that said, I don't think it will improve performance at all due to the max depth being 5 and a total of 25 blocks will ever be scanned.

Defman commented 4 years ago

Terminology:

  1. Weak block: a block which water can override/destroy
  2. Drain: nearest weak blocks which are on a lower y level and within 5 blocks range.
  3. Flowable block: a fluid block which liquid level is low enough such that it is still able to flow.

Algorithm

  1. On block update, schedule flow update for itself and all surrounding flowable blocks.
  2. When the schedule is triggered use BFS to find all drains.
  3. If no drains exist to try to spread water on all faces. else try to spread water on all faces which decrease the manhattan distance to any drain.
  4. If there's a weak block below, spread.

I also have to allow drains to be located through other liquids, I'm not sure how Minecraft fluid logic works in that regard.

Waterlogged blocks, luckily the only block which water can flow to and become waterlogged is item frame, which is not a block but an item frame. All other waterlogged blocks have to be a source.

Defman commented 4 years ago

Furthermore, all fluid entities should be saved on server shutdown such that water does not frezz and stops flowing.