River generation

Started by Sinjin, July 25, 2023, 00:41:16

Previous topic - Next topic

Sinjin

Hi, I want to generate a river based on the sides of an image. I have only 2 images, in one the river goes straight from left to right, on the other picture it goes from down to right. Now i want to flip and rotate 90 degree only, that way every direction would be possible. I have this code, but it kinda totally goes wrong, I cant do it, its harder than I thought or im thinking the wrong way.

type triver
  field pic:timage
  field x%,y%
  field flips% 'bit 0=flipx, bit 1=flipy
  field rot% 'can be 0 or 90
  field link%[4] 'direction of flow, 0=no connection, 1=source, 2=destination
                 'up right down left 'maybe left right up down?

  method draw()
    setscale 1-(flips&1) shl 1,1-(flips&2)
    setrotation rot
    drawimage pic,x,y
  endmethod
endtype

automidhandle true
global river:triver[2]
river[0]=new triver
river
  • .link=[0,2,0,1] 'arrow from left to right
river
  • .pic=loadimage("p1.png") '64*64
river[1]=new triver
river[1].link=[0,2,1,0] 'arrow from down to right
river[1].pic=loadimage("p2.png") '64*64

global path:triver[]
global x%=maxx2,y%=maxy2
path=makepath()
function makepath:triver[]()
  local p:triver[1]
  local thisriver:triver=new triver
  local rn%=rnd(2)
  thisriver.pic=river[rn].pic
  thisriver.link=river[rn].link
  thisriver.flips=rnd(4)
  thisriver.rot=int(rnd(2))*90
  thisriver.x=x
  thisriver.y=y
  p
  • =thisriver

  local pathlen%=10
  while pathlen
    pathlen:-1
'get direction
    local dir%=4 ',direction%
    while dir
      dir:-1
'      if thisriver.link[(dir+thisriver.flips+(thisriver.rot=90))&3]=2 then exit
      local b%
      select dir
      case 0,2 b=(thisriver.flips&1) shl 1
      case 1,3 b=(thisriver.flips&2)
      endselect
      b=(b+dir+(thisriver.rot=90)) mod 4
      if thisriver.link=2 then exit
    wend
'    direction+something
    select dir
    case 0  y:-64
    case 1  x:+64
    case 2  y:+64
    case 3  x:-64
    endselect
    thisriver=new triver
    rn=rnd(2)
    thisriver.pic=river[rn].pic
    thisriver.link=river[rn].link
    thisriver.x=x
    thisriver.y=y
'get direction from last direction for new tile
    local cnt%[8],ac%
    for local r0%=0 to 1
    for local a0%=0 to 3
      if (thisriver.link[(a0+r0)&3]=1) and (dir=a0) then 'and direction+next direction<=90
        cnt[ac]=a0+r0 shl 2
        ac:+1
      endif
    next
    next
    rn=rnd(8)
    thisriver.flips=cnt[rn]&3
    thisriver.rot=cnt[rn] shr 2*90
    p=p[..p.length+1]
    p[p.length-1]=thisriver
  wend
  return p
endfunction


Sinjin

#1
I kinda fixed the 1st part of the loop by allowing 4 rotations but only 1 flip, but I dont get the second part. Also I need to limit the general direction of the path so it never crosses itself nor it gets too weird. Well, it could cross if you allow it in the link field depending on the tiles you have.

type triver
  field pic:timage
  field x%,y%
  field flips% 'bit 0=flipx
  field rot% '0 to 270 step 90
  field link%[4] 'direction of flow, 0=no connection, 1=source, 2=destination
                 'up right down left

  method draw()
    settransform rot*90,1-flips shl 1,1
    drawimage pic,x,y
    settransform 0,1,1
drawtext rot+":"+flips,x-30,y+10
  endmethod
endtype

automidhandle true
global river:triver[2]
river[0]=new triver
river[0].link=[0,2,0,1] 'arrow from left to right
river[0].pic=loadimage("p1.png")
river[1]=new triver
river[1].link=[0,2,1,0] 'arrow from down to right
river[1].pic=loadimage("p2.png")

global path:triver[]
global x%=maxx2,y%=maxy2
path=makepath()
function makepath:triver[]()
  local p:triver[1]
  p[0]=new triver
  local thisriver:triver=p[0]
  local rn%=rnd(river.length)
  thisriver.pic=river[rn].pic
  thisriver.link=river[rn].link
  thisriver.flips=rnd(2)
  thisriver.rot=rnd(4)
  thisriver.x=x
  thisriver.y=y

  local pathlen%=1,generaldirection%
  while pathlen
    pathlen:-1
'get direction
    local dir%=4
    while dir
      dir:-1
      if thisriver.link[dir-thisriver.rot+thisriver.flips shl 1 &3]=2 then exit
    wend
'    generaldirection:+something
    select dir
    case 0  y:-64
    case 1  x:+64
    case 2  y:+64
    case 3  x:-64
    endselect
    local dir2%=(dir+2) mod 4'~(thisriver.flips)

    p=p[..p.length+1]
    p[p.length-1]=new triver
    thisriver=p[p.length-1]
    local thisdirection%
    repeat
      rn=rnd(river.length)
      thisriver.pic=river[rn].pic
      thisriver.link=river[rn].link
      thisriver.x=x
      thisriver.y=y
'get direction from last direction for new tile
      local cnt%[8],ac%
      for local a0%=0 to 1
      for local r0%=0 to 3
        if (thisriver.link[dir+r0~a0&3]=1) and (thisriver.link[r0~a0]=2) then
          cnt[ac]=a0+r0 shl 1
          ac:+1
        endif
      next
      next
      thisriver.flips=cnt[rn]&1
      thisriver.rot=cnt[rn] shr 1
'      thisdirection=something
    until true 'generdirection+thisdirection<=180 and >=-180
  wend
  return p
endfunction

Midimaster

#2
Can you add the PNG files? It is hard to understand what you are doing as long as we cannot see, how these" river-pictures" look.
f.e. I  understood that yo have two pictures a STRAIGHT and a CURVE.

I was not able to understand what your problem is. What do you mean with "the second part"? Is it the path-finding or a graphic problem. Which bugs do happen?

And can you also send the main loop? There are a lot of parameters in the function, that seems to defined outside. What is River.Length? It is not a field of the TYPE TRiver. Where is it defined?



So we can play with the code.

Why do you not wok with TList, but an array?


Here are some of my ideas. This is an algo, that tries to build a river and not touch existing elements. At the moment the tributaries are not added. So it is only a single long river. The creation runs often godd, but still runs in situations without an exit. watch:

SuperStrict
Graphics 400,400

Const GRID_SIZE:Int=10, MAX_X:Int=38, MAX_Y:Int=38
Const WEST:Int=0, NORTH:Int=1, EAST:Int=2, SOUTH:Int=3
Const STRAIGHT:Int=0, TO_LEFT:Int=1, TO_RIGHT:Int=2

 TRiver.RandomRiver 20,20

Repeat
Cls
TRiver.DrawAll
Flip 1
Delay 300
TRiver(TRiver.River.Last()).GoUpstream()
Until AppTerminate()


Type TRiver
Global River:TList=New TList

Field X:Int, y:Int
Field Connection:Int
Field Direction:Int

Function RandomRiver:TRiver(X:Int, y:Int)
Local Mouth:TRiver=New TRiver
Mouth.X = X
Mouth.Y = Y
Mouth.Direction = Rand(WEST,SOUTH)   '  0= WEST, 1=NORTH, 2=EAST, 3=SOUTH
River.AddLast Mouth
'Mouth.GoUpstream()
End Function


Function CheckFreeLand:Int (X:Int,Y:Int)
'checks if a land contains already a river there
Print "check at x:" + x + " y:" + y
If X<1     Or y<1     Then Return False
If X>MAX_X Or Y>MAX_Y Then Return False

'checking 9 fields around the test field:
Local count:Int
For Local xAdd:Int=-1 To 1
For Local yAdd:Int=-1 To 1
For Local loc:TRiver = EachIn River
'Print "loc" + loc.x + " " + loc.y
If (loc.X=X+xAdd) And (loc.Y=Y+yAdd)
count:+1
EndIf
Next
Next
Next
If count>2 Then Return False
Print "check passed: free land: RIVER added"
Return True
End Function

Method GoUpstream()
Print "--------------------------------------"
Print "River try to go upstream (element no:" + TRiver.River.Count() + ")"

Connection = Rand(3,10)  'no code for tributaries at the moment
Select Connection
'Case TO_LEFT
' left tributary (related to prior river element)
'AddRiver Direction
'Case TO_RIGHT
' right tributary (related to prior river element)
'AddRiver Direction
Default
Connection=0  'continue this river
AddRiver Rand(1,10)  ' 10% chance to change direction
End Select
End Method


Method AddRiver(NewDirection:Int)
Local NewElement:TRiver= New TRiver

If NewDirection>TO_RIGHT Then NewDirection = STRAIGHT
Select NewDirection
Case STRAIGHT
Print "try Ahead=" + direction
NewElement.Direction = Direction
Case TO_LEFT
Print "try turn left"
NewElement.Direction = (Direction+3) Mod 4
Case TO_RIGHT
Print "try turn right"
NewElement.Direction = (Direction+1) Mod 4
End Select

NewElement.X = CalculateNewX(NewElement.Direction)
NewElement.Y = CalculateNewY(NewElement.Direction)
If CheckFreeLand(NewElement.X,NewElement.Y)=False Then Return
River.AddLast NewElement
End Method


Method CalculateNewX:Int(NewDirection:Int)
Select NewDirection
Case WEST
Return X-1
Case EAST
Return X+1
Default
Return X
End Select
End Method


Method CalculateNewY:Int(NewDirection:Int)
Select NewDirection
Case NORTH
Return Y-1
Case SOUTH
Return Y+1
Default
Return Y
End Select
End Method


Function DrawAll()
For Local loc:TRiver = EachIn River
loc.Draw
Next
End Function


Method Draw()
DrawRect X*GRID_SIZE,Y*GRID_SIZE, GRID_SIZE, GRID_SIZE
End Method
End Type


The check for collision checks 9 fields around the text field. And if there are more than 2 filled with water it denies the new river part.

2 is important, because in a situation where the river turn there can be 2 elements of its own in the vicinity:

example: River comes from WEST and turns to NORTH (1,2, T=Testing field)
----------------
|    |    |    |
----------------
|    | T  |    |
----------------
| 1  | 2  |    |
----------------
...back from Egypt

Pakz

What helped me understand map generation a lot more was to start looking at it as a painters situation. You just program a painters brush to do the effect you are going for.

There was a article on river generation I read a while ago for civilization style games. But I have no idea where that is at now.

Sinjin

@Pakz In the old days it was called turtledraw? Like when your brush always draws from the last position.
@Midimaster Here are the pics I use, of course in my game its an animation.

Midimaster

Thank you. That what I read from your code. So it will be a tile-based screen. Now I think my example can help you.

Its a pity you didn't answer my other questions. Which problem did you observe in your code? Can you send the whole code? etc...
...back from Egypt

Sinjin

Oh sry, I just fixed it, it didnt pick the correct tile for the "1" where it comes from. Now i can implement something like a generaldirection variable so it wont cross.

I changed this line
        if (thisriver.link[dir+r0~a0&3]=1) and (thisriver.link[r0~a0]=2) thento this. So close and yet so far :)
        if (thisriver.link[r0+a0 shl 1-dir&3]=1) then

Sinjin

#7
False alarm, I have a bug there. It's still not working correctly. But I think i just miss one flip somewhere.
      rn=rnd(ac)
      thisriver.flips=cnt[rn]&1
      thisriver.rot=(cnt[rn] shr 1)*90

Midimaster

I wrote an example that needs 3 PNGs...

LEFT TURN: RiverCL.png      RIGHT TURN: RiverCR.png      STRAIGHT: RiverS.png


.... to get along without the flips:


SuperStrict
Graphics 1000,800
AutoMidHandle True
Global PicS:TImage=LoadImage("RiverS.png")
Global PicL:TImage=LoadImage("RiverCL.png")
Global PicR:TImage=LoadImage("RiverCR.png")

Const GRID_SIZE:Int=10, MAX_X:Int=38, MAX_Y:Int=38
Const WEST:Int=0, NORTH:Int=1, EAST:Int=2, SOUTH:Int=3
Const STRAIGHT:Int=0, TO_LEFT:Int=1, TO_RIGHT:Int=2

TRiver.RandomRiver 10,10
 
Repeat
Cls
TRiver.DrawAll
Flip 1
Delay 250
TRiver(TRiver.River.Last()).GoUpstream()
Until AppTerminate()


Type TRiver
Global River:TList=New TList

Field X:Int, y:Int
Field Connection:Int
Field Direction:Int
Field Child:TRiver

Function RandomRiver:TRiver(X:Int, y:Int)
Local Mouth:TRiver=New TRiver
Mouth.X = X
Mouth.Y = Y
Mouth.Direction = Rand(WEST,SOUTH)   '  0= WEST, 1=NORTH, 2=EAST, 3=SOUTH
River.AddLast Mouth
'Mouth.GoUpstream()
End Function


Function CheckFreeLand:Int (X:Int,Y:Int)
'checks if a land contains already a river there
Print "check at x:" + x + " y:" + y
If X<1     Or y<1     Then Return False
If X>MAX_X Or Y>MAX_Y Then Return False

'checking 9 fields around the test field:
Local count:Int
For Local xAdd:Int=-1 To 1
For Local yAdd:Int=-1 To 1
For Local loc:TRiver = EachIn River
'Print "loc" + loc.x + " " + loc.y
If (loc.X=X+xAdd) And (loc.Y=Y+yAdd)
count:+1
EndIf
Next
Next
Next
If count>2 Then Return False
Print "check passed: free land: RIVER added"
Return True
End Function

Method GoUpstream()
Print "--------------------------------------"
Print "River try to go upstream (element no:" + TRiver.River.Count() + ")"

Connection = Rand(3,10)  'no code for tributaries at the moment
Select Connection
'Case TO_LEFT
' left tributary (related to prior river element)
'AddRiver Direction
'Case TO_RIGHT
' right tributary (related to prior river element)
'AddRiver Direction
Default
Connection=0  'continue this river
AddRiver Rand(1,10)  ' 10% chance to change direction
End Select
End Method


Method AddRiver(NewDirection:Int)
Local NewElement:TRiver= New TRiver

If NewDirection>TO_RIGHT Then NewDirection = STRAIGHT
Select NewDirection
Case STRAIGHT
Print "try Ahead=" + direction
NewElement.Direction = Direction
Case TO_LEFT
Print "try turn left"
NewElement.Direction = (Direction+3) Mod 4
Case TO_RIGHT
Print "try turn right"
NewElement.Direction = (Direction+1) Mod 4
End Select

NewElement.X = CalculateNewX(NewElement.Direction)
NewElement.Y = CalculateNewY(NewElement.Direction)
If CheckFreeLand(NewElement.X,NewElement.Y)=False Then Return
River.AddLast NewElement
Self.Child = NewElement
End Method


Method CalculateNewX:Int(NewDirection:Int)
Select NewDirection
Case WEST
Return X-1
Case EAST
Return X+1
Default
Return X
End Select
End Method


Method CalculateNewY:Int(NewDirection:Int)
Select NewDirection
Case NORTH
Return Y-1
Case SOUTH
Return Y+1
Default
Return Y
End Select
End Method


Function DrawAll()
For Local loc:TRiver = EachIn River
loc.Draw
Next
End Function


Method Draw()
Print "self orientation:" + Direction + " next: " + Child.Direction
SetScale 0.5,0.5
SetRotation ((Direction+2)*90)  Mod 360
Local diff:Int = direction-Child.direction
If diff=0 Or diff<-999
DrawImage picS,X*32,y*32
ElseIf diff=1 Or diff=-3
DrawImage picL,X*32,y*32
Else
DrawImage picR,X*32,y*32

EndIf
End Method
End Type

GNetTPlayer.gif
...back from Egypt

Sinjin

I erased it all and made it similar to yours :). I even preserved the flip just by making a new tile with the same image but different direction tag. My little game looks like this now. I got no end condition for the river just yet, but thx.

Sinjin

#10
thanks, my game looks like this now:

and yes all those little monkeys/animals do their stuff for now, they even steal stuff lol but i still wish i could implement AI ^^ i gave it up. its like stranded..we all know that one but in 2d. less cpu :D also its super hard to animate all that, i mean im not really interested in graphics but i do what i can. almost like rimworld

Derron

There is no need to implement a "proper" AI.
I guess if you add "weighting" to the "AI" decisions, then they already could behave "differently" and also are able to react to the environment (simply said: "player attacked me?" weighting for "frightened" or "disliking humans" changes).
As all these neural nets are also just "weightings" for a plenitude of parameters, this is the very same.
You can even add "layers" in the sense of "dependencies" (disliking humans inherits from "disliking something" and "disliking something" might be affected from a base character value ... ).

Next to "behaviour" you would also simulate "needs". Needs like "thirst" are of course also "basic values" which can be influenced ("modified"). So "being afraid" could cut the "effective" values of basic needs etc. The apes are thirsty but afraid, so won't go drinking at the river until "thirst * afraidMod > 0.x" (too thirsty to ignore...).

I guess you already have similar things in it. I bet it is quicker than using some kind of "neural net" based on some recurrent neural net learnings ("learn to play the game"). Such stuff is still rather expensive to implement (gpu and cpu).


bye
Ron

Sinjin

I know there is "no need" for real AI and I would implement it only for one npc to make the game more interesting. I still think about it... It wont need 100000 neurons, but again, if there were...lets say a female char, that would be more than interesting in a game like that ^^ although, since Im not sure what winning condition I implement, if any... like I said, it would become more interesting. But thanks for the help so far again.