yairm210 / Unciv

Open-source Android/Desktop remake of Civ V
Mozilla Public License 2.0
8.02k stars 1.51k forks source link

Add rivers #125

Closed yairm210 closed 4 years ago

yairm210 commented 6 years ago

Not sure how to do this, technichally

rsubtil commented 6 years ago

Hey, I'm really interested in this project and would like to help. I believe the rivers would be between grids and not inside them, right (as in Civ V)?

yairm210 commented 6 years ago

Yes, and therein lies the problem. Two in fact. The first is, how do I represent rivers? We basically have a 2d grid for tiles, but what coords do we give rivers, that lie between 2 tiles? Or do we mark them by the tike intersections, between 3 tiles?

rsubtil commented 6 years ago

I have yet too look deeper into the code, but could a property be set on a tile to define if in one of it's borders it has a river, and if yes in which border?

About how to represent rivers, if the hex tiles have a solid and colored border, would it be possible to make a border section blue?

I'm just tossing around ideas, I'll look at the implementation and see if I can come up with something.

yairm210 commented 6 years ago

The problem is that it's a shared river, so it 'belongs' to both tiles. Yes, if there's no other way, you could add the 'river at direction x' to both the tiles, but that's a bit ugly, I think.

Am-per-Sand commented 6 years ago

AFAIU you would like to have a database in the code to be able to be checked versus particular tiles, and it should be independent of the tile coordinates. From what you ve written above rivers are like empire boundaries, with sections defined by two neighbouring tiles, and a tilemay have 0 to 5(6?) rivers aside. I don't know your code structure (and can't read it from the files repository here, too ignorant for that), but given the above, I wonder when does the program need to check rivers positions database: when moving units? when calculating tile yields? If you precise that, may be someone (or I) will be able to suggest something you will find acceptable or inspiring.

jacksondus commented 6 years ago

@yairm210 If there is a method to get neighboring tiles, you could just loop through your tiles and use a (relatively low-chance) random value to determine whether it has a river. Then you get a random tile and "add a river to it."

In pseudocode this might look like:

for(Hex h : tiles)
{
    if(randomValue(1%) && !h.ocean)
    {
        Hex[] nbrs = h.GetNeighbors();
        Hex nbr = RandomNbr(nbrs);
        h.addRiverCrossing(nbr);
        nbr.addRiverCrossing(h);
        Game.gfxRiverCrossing(new GraohicalRiverCrossing(h, nbr));
    }
    else
    {
        if(nbrHasRiverCrossing(h) && randomValue(30%) && !h.ocean)
            Hex[] nbrs = h.GetNeighbors();
            Hex nbr = RandomNbr(nbrs);
            h.addRiverCrossing(nbr);
            nbr.addRiverCrossing(h);
            Game.gfxRiverCrossing(new GfxRiverCrossing(h, nbr));
    }
}
jacksondus commented 6 years ago

I'd help with code myself but my PC is broken and isn't getting fixed/replaced for a couple weeks.

DeafIllDryFur commented 5 years ago

I have yet to go through your code thoroughly enough to understand how the tiles are organized. For this comment I presume each tile has an x and a y coordinate, as this is sufficient information to build a hex map. If this is the case, the most slim option I can think to define a river is to give each 3-tile junction also (x,y) coordinates that corresponds to a participating tile in a certain direction you define. For this example let's say it is the junction at 6 o'clock from the tile with the same coordinates. That would let you build a river with the same logic of neighbouring junctions your tiles should use for neighbourhood relations. If you picture your whole map shifted by half a tile downward, the validity of my proposed approach should become clear. You won't get into trouble with coordinates not resembling a tile unless you want a river to start at an edge-of-the-world junction, which you could simply forbid (or catch, if you want it to work that way).

I like this project and I would love to contribute when I have more time, which should be around February. I'd love to work on the opponent AI, as that is also my field of profession, but any game logic stuff that needs work would be cool also. Let me know what you think about that.

DeafIllDryFur commented 5 years ago

Actually, I have visualized that for myself and seen my mistake. There are double the number of junctions when compared to the number of tiles, so for each tile you would need two junctions defined as rivers. What is more, in the game there can be two junctions who are rivers which are not actually connected by a "river" edge, so just checking for neighbouring junctions being rivers will not suffice. Also, it probably did not help that the tiles in UnCiv actually are placed such that there is no 6 o'clock junction, but there is a downward left and downward right junction we could use.

Pseudocode for defining rivers based on tile (x,y) coordinates, as long as they are linear:

function that defines a tile {
    ...
    byte river[2]; //river is the variable defining the river status of the two southern junctions
    river [0] = 0; //left junction is no river
    river [1] = 3; //right junction belongs to river 3
    ...
}

Plotting a river on the map would then be handled by checking both junctions for their river numbers and painting the downmost edge blue if they both are equal (and greater 0) as well as checking the upward edges

for (tile t with coordinates (x,y)) {
    if (t.river[0] > 0) {
        if (t.river[0] == t.river[1]) paintBlue(edgeBetween((x,y),(x,y+1)); //assuming y counts up when moving downwards on the map
       if (t.river[0] == (x-1,y).river[1]) paintBlue(edgeBetween((x,y),(x-1,y+1));
    }
    if (t.river[1] > 0 && t.river[1] == (x+1,y-1).river[0]) paintBlue(edgeBetween((x,y),(x+1,y));
}

This might be cumbersome to check for game mechanism impacts of rivers, so you might want to save the downmost 3 edges as river states for each tile instead byte river[3];. The memory impact of 1 byte more to store as river identifier should be negligible to the programming comfort you should have when just checking the upper of the two tiles for a river when e.g. moving instead of the junction identifiers of two tiles.

yairm210 commented 5 years ago

Since rivers in Civ are defined by edges, not intersections, I was thinking that each tile "takes control" of the three edges above it, that is - North, northeast, Northwest Giving us a bool[3] on each tile, as it were, signifying existence or non existence of a river there

DeafIllDryFur commented 5 years ago

Hey,

 

yeah, as I said in my last paragraph, that would also be a possibility and likely be easier to handle for any mechanism that needs to check for rivers nearby, like tile yield, building requirement and movement. What is more is that you don't need a number, but a boolean variable for each of the three edges will suffice.

 

Cheers

 

Gesendet: Sonntag, 21. Oktober 2018 um 18:57 Uhr Von: yairm210 notifications@github.com An: yairm210/UnCiv UnCiv@noreply.github.com Cc: DeafIllDryFur josuazscheile@web.de, Comment comment@noreply.github.com Betreff: Re: [yairm210/UnCiv] Add rivers (#125)

Since rivers in Civ are defined by edges, not intersections, I was thinking that each time "takes control" of the three eyes above I, that is - North, northeast, Northwest

— You are receiving this because you commented. Reply to this email directly, view it on GitHub, or mute the thread.

Crit-Cee commented 5 years ago

You could create a whole new tile set that has the resource values for the river and visually half a river on the edge of the hex. So you would have a plains-rive hex (having half the river on it's edge) and another plains-river hex (or hill-river, tundra-river, etc...) next to it so that the river edges aline to make a whole river on their edges.

That could be a nightmare coding so that you would always get the river edges lining up next to each other, and then on top of that have continuous progression of these rive tiles to a body of water. I suck at algorithm generation, so unfortunately I can't help with that.

zivvel commented 5 years ago

Your bool[3] will work and track rivers' existence at only one place, but you'll never have a river along the southern 3 edges of a map. bool[6] is 3 more bits per tile and probably worth it.

yairm210 commented 5 years ago

@zivvel au contraire, the South of tile A is always comprised of norths of tiles B,C,D If you have bool[6] there is no longer a single source of truth to whether there is a river on the edge - if one side thinks there is and the other thinks there isn't, we've got a problem

zivvel commented 5 years ago

Right, I like the single source of truth, but are there tiles south of the southern edge of the world map?

yairm210 commented 5 years ago

There was be no rivers on the edges of the map, so that shouldn't be a problem

Deyvidy commented 5 years ago

I believe this will help you https://www.redblobgames.com/maps/mapgen2/

yairm210 commented 5 years ago

I've read everything in redblob, not that helpful for our specific needs :/

PaulRitter commented 5 years ago

Since this is still open, I'm gonna throw in my two cents.

How to define 'em

I suggest you define rivers by saving the two tiles it intersects, preferably the position if the two tiles. This would mean that every riverunit would be a array of two Vector2's. This way you can add a movement debuff more easily, you would just need add a check in

getMovementCostBetweenAdjacentTiles()

I'd suggest to rewrite the function to just return the total cost at the end, because a river would just add to the cost, being a bit of a pain in the arse with your current cost calculation.

All this should be quite easy to implement, if you would just convert all if-statements to else if's. Then just add the river cost at the end if thats the case and boom, you got it.

Saving them

I'd recommend creating a new list in TileMap.kt. You may add a struct for saving the rivers, but you could also get away with just saving a bunch of arrays, since I do not see any other relevant information that needs to be saved about a river.

Generating rivers

This I cannot help you with, but keep in mind that you can always generate rivers and then convert them into this format, as I can imagine it would be kinda of a pain in the arse to generate them directly into this format, with checking for adjacent rivers and all, which brings me to the last part:

Rendering

Yeah I don't have much Ideas here really, I'm more of a backend guy. One thought I would share is that you could just save entire rivers by saving them as groups of multiple riversegments, saved using the method I explained above (two Vector2's). Might be usefull if you want to connect the river in the corners, might also be absolutely useless for rendering, as I said, I have no idea. You could always just paint the borders blue.

Well this turned out to be more comprehensive as I planned, I hope it helps.

Enjoy

Ps: Willing to help you on this, but before today I've never even heard about Kotlin, so I think me helping code would make things worse.

vincemolnar commented 4 years ago

I think the easiest solution would be to simply mark hexes with a single boolean value to say if they are adjacent to a river or not. Checking whether there is a river in between two tiles is then equivalent to checking if they are both adjacent to a river, and the tiles at the ends of the common edge are either sea/ocean/lake(/spring?) tiles or also adjacent to a river. A river would then be defined by its banks (tiles marked as adjacent to a river), its source(s) (lakes or springs, which might just be a placeholder for the source of the river or a proper tile type) and its target (a water tile). Certainly this does not allow any topology and would sometimes introduce islands where two parallel rivers are joined, but I think it's acceptable and also easy to generate.

Lytecyde commented 4 years ago

Rivers are joined sometimes in the real world too, perhaps if loops develop they could be converted to lakes!

Lytecyde commented 4 years ago

rivers should be difficult to pass until boats/riverboats are invented then they should advance trade and ease travel.

suryatjo commented 4 years ago

Just some ideas for rivers. It can be put inside a tile and it should begin with a mountain or a spring. Wouldn't it be easier to code?

yairm210 commented 4 years ago

If only that was how rivers worked in Civ... No, they need to be between tiles. I'll add a new issue regarding the current problem with rivers.