2021-05-24 16:33:45 (edited by amerikranian 2021-05-24 16:34:38)

So, I've been looking into noise functions and their outputs. One thing I've come across is tweaking values. For example, maybe your first run generates only three out of the 10 types of terrain on a map. My question is, how would one go about efficiently communicating this information through speech / audio? Sighted folks can just go and graph the resulting output and look at its distribution. We... kind of can't do the latter portion of this. We can graph using something like Matplotlib, but we can't actually see the output. Has anybody ran into this issue or even considered how to go about eliminating this barrier?
Another problem that I have discovered is ambience placement. Imagine a 16 by 16 sector of mostly grass. On the right hand side we have a river (maybe 3 or so tiles of it). How would one go about playing a sound at this location to let the player know that there is a body of water nearby? I'm asking because it is a convention in Audiogames to place marker sounds to identify significant features like streams, outside areas, etc. The reason for this question is that, if I understand this correctly, the entirety of the map has not been generated. The procedural creation creates blueprints which ensure that, when the player enters a sector, said area would have specific features. This is mostly theory, mind you, more to satisfy curiosity than anything.
Ambience placement is not a unique problem to procedural generation. What if I generate a maze and wish to place ambiences around it? What if I implement the Game of Life and use it to generate my levels? How would I go about placing sound sources in the result?

2021-05-24 17:00:28

Well, BK3 already has a camera.  My solution for this in the future will be that, just with different sounds.  You can probably do a lot of cool things with something like the VOICe but modified for terrain though, and a "70% forest in this sector" description isn't very hard.

Doing ambiance without higher-level features is going to be tricky.  That is to say that if you make a river you probably want to remember that there is a river and what the overall shape of it is.  Failing that you'll need to do some postprocessing on the terrain to figure it out, e.g. floodfill algorithms that can tell you that all of these river tiles are one river.  It's not super hard to take a bunch of terrain and turn it into bounding boxes or something, but as far as I know no one has done it and I doubt that there's a generally applicable solution.  The only things I can think of that have gone all out with procedural generation are muds, though maybe castaways was random, I can't remember.

My Blog
Twitter: @ajhicks1992

2021-05-24 20:19:52

Camlorn has the right idea with sector type density. I have a procedural map generator that produces some big maps, it was designed to do it in layers of terrain types and I could set what percentage of the map was filled by each type of terrain. Breaking it down into individual sector density by an X by X area wouldn't have been that difficult. Alternatively, i've mentioned before how BrushTone could potentially be used as a map editor, which could be used to create maps from image arrays, but using it to view 2D map data is also possible. Assuming you assign each of your 10 types of terrain as one color, you could output your map as a PNG, and then use row by row scanning or sonify only specific colors to get a general sense of the layout and density of your map. If you like, I could throw together a quick script for exporting a 2D array into an image format.

-BrushTone v1.3.3: Accessible Paint Tool
-AudiMesh3D v1.0.0: Accessible 3D Model Viewer

2021-05-24 20:52:50

I'm not proposing sector density.  I hate to use ascii art, but this is small, so:

r f f
r f f
r r r

If r is river and f is forest, then you can pick a random river tile and do a floodfill algorithm to assign each tile to something representing a river.  After that, you can take your river tiles, and convert them to a set of larger bounding boxes.  This gives you semantic information about the river, and you can then ask interesting questions like "what is the closest point to the player?"

Alternatively, after assigning regions you can do raycasting and see when rays cross tiles for which you want to do ambiance, then do some post-processing on that.  E.g. cast every degree in a circle around the player, then deduplicate the set so that you only hold the closest point for each region.

My Blog
Twitter: @ajhicks1992

2021-05-24 23:38:38

Hmm, so something equivalent to a BSP or Quadtree perhaps?

-BrushTone v1.3.3: Accessible Paint Tool
-AudiMesh3D v1.0.0: Accessible 3D Model Viewer

2021-05-25 00:03:25

Yeah, you could back it with that if you wanted.  To me the spatial partitioning strategy once you have your set of boxes is much less interesting than getting that far, because those problems are solved enough that you can either find libraries or pseudocode on Google no problem.

I will say that at Amerikranian's level of experience though, raycasting is probably easier.  Except that you'll probably need Cython because to do that you need to raycast at minimum hundreds of rays per tick.  me and a friend are porting this to Rust at the moment.  It's not actually simple to raycast on a grid, it's simpler, but you can just copy that line for line to whatever language and it should work, and it's correct to the best of my knowledge (and soon I will have a test suite proving that).

My Blog
Twitter: @ajhicks1992

2021-05-25 19:41:14

@magurp244
I'd love to ask more questions about your experiences with the topic. I sent you a pm.

Trying to free my mind before the end of the world.

2021-05-30 02:36:04 (edited by magurp244 2021-05-30 02:52:04)

Ooof! Well someone asked for an example of map procedural generation, so i've taken a bit of time to excavate and refactor a module I made awhile ago. The good news: It will generate a map of any size or dimensions you want, but bigger = longer, and the terrain consists only of water, shore, and land. You can set the density of the land mass, or adjust the number of land nodes, and when finished it will spit out a dandy little PNG image you can look at or edit with BrushTone. However, be warned that BrushTone is a 32 bit app, so can't handle anything over 4000 by 4000. Anyhoo:

import pyglet
from pyglet.window import key
from pyglet.gl import *
import numpy
import random
import sys

class Prototype(pyglet.window.Window):
    def __init__(self):
        super(Prototype, self).__init__(640, 480, resizable=False, fullscreen=False, caption="Test")
        self.clear()

        self.map = Map_Gen(128,128)
        self.map.generate()

        pyglet.clock.schedule_interval(self.update, .01)



    def update(self,dt):
    #draw screen
        self.draw()



    def draw(self):
        self.clear()
        self.map.minimap['map'].blit(32,0)
        


    def on_key_press(self,symbol,modifiers):
        if symbol == key.ESCAPE:
            self.close()





#Map Generator
#/---------------------------------------------------------------------------/
class Map_Gen(object):
    def __init__(self,mx=64,my=64,density=0.5,r_density=0.5,node=10):
    #Map Dimensions
        self.mx = mx
        self.my = my
    #density of terrain: 0.0 to 1.0
        self.density = density
    #number of terrain nodes
        self.node = node
    #random map seed
        self.seed = None

    #minimap main texture
        self.minimap = {'map_image':pyglet.image.create(1,1),'map':pyglet.image.create(1,1)}


    #contains the ground layer data that makes up the map
        self.map = [] #28
    #contains the object layer information IE: vehicles, resources, objects, components, (buildings?),etc.
        self.resource = []

    #The tileset is an indexed list of tile types based on their shape and purpose relative to terrain placement.
    #In this case, the total is 16 tiles with the order being:

        #0: Land
        #1: Water

        #2: Water with lower right corner Land
        #3: Water with bottom half Land
        #4: Water with lower left Land
        #5: Water with right side Land
        #6: Water with left side Land
        #7: Water with upper right Land
        #8: Water with upper half Land
        #9: Water with upper left Land

        #10: Land with Bottom Right Water
        #11: Land with bottom left water
        #12: Land with upper right Water
        #13: Land with upper left water

        #14: Land with Upper Right and Lower Left water
        #15: Land with upper left and lower right water

        self.tileset = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]

    #Tile properties determines whether the tiles are passable or solid, the length equals number
    #of different types of tile, 16 in all: #0 passable, 1 impassable, 2 overhead
    #tiles that have water are impassable.
        self.properties = [0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]


    #The update table is a quick reference 2D array to change adjacent tiles during a change based on
    #what tiles are adjacent to the new ground tile. So if the tile above the new ground tile is solid water, it would be
    #changed to water with the bottom being land, or self.table[8_direction][current_land_type] = new_land_type.
    #update table
        self.table = [[0,2,2,3,3,5,12,5,11,14,0,11,12,13,14,13],#upper left
                      [0,3,3,3,3,13,12,13,0,12,0,0,12,13,12,13],#top
                      [0,4,3,3,4,13,6,15,10,6,10,0,12,13,12,15],#upper right
                      [0,6,12,12,6,0,6,10,10,6,10,0,12,0,12,10],#right
                      [0,9,14,12,6,11,6,8,8,9,10,11,12,0,14,10],#lower right
                      [0,8,11,0,10,11,10,8,8,8,10,11,8,8,11,10],#bottom
                      [0,7,5,13,15,5,10,7,8,8,10,11,0,13,11,15],#lower left
                      [0,5,5,13,13,5,0,5,11,11,0,11,0,13,11,13]]#left


    #Toggle map wrap around on/off 
        self.wrap_toggle = False

    #toggle terrain update
        self.update = False

    #total solid water count
        self.w_count = self.mx * self.my
    #total shoreline count
        self.s_count = 0
    #total solid ground count
        self.g_count = 0

    #wander variables
        self.x = 0
        self.y = 0
        self.counter = 0



#Generate map data and map image
#/-----------------------------------------------------------------------------/
    def generate(self,seed=None):
    #generate seed
        if seed == None:
            self.seed = random.randint(-999999999999,999999999999) #move to window?
        print("SEED: ", self.seed)
        random.seed(self.seed)

    #generate water
        print("Initializing Environment")
        self.map = numpy.zeros((self.my,self.mx),dtype='uint8')
        self.map[:] = 1

    #generate land masses
        print("Generating Land Masses")
        dx = random.randrange(0,len(self.map[0])-1,1)#incorporate random positions to help map saturation.
        dy = random.randrange(0,len(self.map)-1,1) 
        direction = 0
        count = 0
    #randomize direction and change terrain accordingly
        while self.g_count < (self.mx*self.my)*self.density:
            direction = random.randrange(0,4,1)

        #right
            if direction == 0 and (dx + 1) < len(self.map[dy])-1:
                dx += 1
        #left
            elif direction == 1 and (dx - 1) > 0:
                dx -= 1
        #up
            elif direction == 2 and (dy + 1) < len(self.map)-1:
                dy += 1
        #down
            elif direction == 3 and (dy - 1) > 0:
                dy -= 1            

        #count number of water/shore/land tiles
        #if water
            if self.map[dy][dx] == 1:
                self.w_count = self.w_count - 1
                self.g_count = self.g_count + 1
        #if shore
            elif self.map[dy][dx] != 0 and self.map[dy][dx] != 1:
                self.s_count = self.s_count - 1
                self.g_count = self.g_count + 1

        #set tile to land and update surrounding tiles with flux
            self.map[dy][dx] = 0
            self.flux(dx,dy)

        #calculate number of nodes and change position based on density threshold NOTE: use self.g_count instead of count?
            count = count + 1
            if count >= ((self.mx * self.my)*self.density)/(self.density*self.node):
                dx = random.randrange(0,len(self.map[0])-1,1)
                dy = random.randrange(0,len(self.map)-1,1)
                count = 0


    #generate minimap
        print("Generating Map")
        self.minimap['map_image'].width = len(self.map[0])
        self.minimap['map_image'].height = len(self.map)

        tmp = numpy.zeros((self.my,self.mx,3),dtype='uint8')
        tmp = numpy.dstack((self.map,tmp))

    #land/water/shore colors
        color = [[139,69,19,255], [0,0,255,255], [100,50,10,255]]
        for a in range(0,15,1):
            if a < 2:
                tmp = numpy.where(tmp[::,::,:1]!=a,tmp,color[a])
            else:
                tmp = numpy.where(tmp[::,::,:1]!=a,tmp,color[2])

        tmp = tmp.astype('uint8')
        
        self.minimap['map_image'].set_data('RGBA',self.minimap['map_image'].width*4,tmp.tobytes())
        self.minimap['map'] = self.minimap['map_image'].get_texture()

    #disable anti-aliasing/bilinear-filtering
        glTexParameteri(self.minimap['map'].target, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
        glTexParameteri(self.minimap['map'].target, GL_TEXTURE_MIN_FILTER, GL_NEAREST)

    #save image
        self.minimap['map'].save('tmp.png')

#update adjacent tiles
#----------------------------------------------------------------------------------|
    def flux(self,x,y):
    #upper left wrap check
        if y+1 > len(self.map)-1 and x-1 < 0:
            mx = self.mx
            my = self.my
        elif y+1 <= len(self.map)-1 and x-1 < 0:
            mx = self.mx
            my = 0
        elif y+1 > len(self.map)-1 and x-1 >= 0:
            mx = 0
            my = self.my
        else:
            mx = 0
            my = 0

        if self.map[(y+1)-my][(x-1)+mx] == 1:
            self.w_count -= 1
            self.s_count += 1
        elif self.map[(y+1)-my][(x-1)+mx] == 10:
            self.s_count -= 1
            self.g_count += 1

    #upper left
        self.map[(y+1)-my][(x-1)+mx] = self.table[0][self.map[(y+1)-my][(x-1)+mx]]



    #up wrap check
        if y+1 > len(self.map)-1:
            my = self.my
        else:
            my = 0

    #up
        if self.map[(y+1)-my][x] == 1:
            self.w_count -= 1
            self.s_count += 1
        elif self.map[(y+1)-my][x] == 8:
            self.s_count -= 1
            self.g_count += 1
        elif self.map[(y+1)-my][x] == 10:
            self.s_count -= 1
            self.g_count += 1
        elif self.map[(y+1)-my][x] == 11:
            self.s_count -= 1
            self.g_count += 1

        self.map[(y+1)-my][x] = self.table[1][self.map[(y+1)-my][x]]



    #upper right wrap check
        if y+1 > len(self.map)-1 and x+1 > len(self.map[0])-1:
            mx = self.mx
            my = self.my
        elif y+1 > len(self.map)-1 and x+1 <= len(self.map[0])-1:
            mx = 0
            my = self.my
        elif y+1 <= len(self.map)-1 and x+1 > len(self.map[0])-1:
            mx = self.mx
            my = 0
        else:
            mx = 0
            my = 0

        if self.map[(y+1)-my][(x+1)-mx] == 1:
            self.w_count -= 1
            self.s_count += 1
        elif self.map[(y+1)-my][(x+1)-mx] == 11:
            self.s_count -= 1
            self.g_count += 1

    #upper right
        self.map[(y+1)-my][(x+1)-mx] = self.table[2][self.map[(y+1)-my][(x+1)-mx]]



    #right wrap check
        if x+1 > len(self.map[0])-1:
            mx = self.mx
        else:
            mx = 0

        if self.map[y][(x+1)-mx] == 1:
            self.w_count -= 1
            self.s_count += 1
        elif self.map[y][(x+1)-mx] == 5:
            self.s_count -= 1
            self.g_count += 1
        elif self.map[y][(x+1)-mx] == 11:
            self.s_count -= 1
            self.g_count += 1
        elif self.map[y][(x+1)-mx] == 13:
            self.s_count -= 1
            self.g_count += 1

    #right
        self.map[y][(x+1)-mx] = self.table[3][self.map[y][(x+1)-mx]]



    #lower right wrap check
        if y-1 < 0 and x+1 > len(self.map[0])-1:
            mx = self.mx
            my = self.my
        elif y-1 >= 0 and x+1 > len(self.map[0])-1:
            mx = self.mx
            my = 0
        elif y-1 < 0 and x+1 <= len(self.map[0])-1:
            mx = 0
            my = self.my
        else:
            mx = 0
            my = 0

        if self.map[(y-1)+my][(x+1)-mx] == 1:
            self.w_count -= 1
            self.s_count += 1
        elif self.map[(y-1)+my][(x+1)-mx] == 13:
            self.s_count -= 1
            self.g_count += 1
        elif self.map[(y-1)+my][(x+1)-mx] == 4:
            self.s_count -= 1
            self.g_count += 1

    #lower right
        self.map[(y-1)+my][(x+1)-mx] = self.table[4][self.map[(y-1)+my][(x+1)-mx]]



    #bottom wrap check
        if y-1 < 0:
            my = self.my
        else:
            my = 0

        if self.map[(y-1)+my][x] == 1:
            self.w_count -= 1
            self.s_count += 1
        elif self.map[(y-1)+my][x] == 3:
            self.s_count -= 1
            self.g_count += 1
        elif self.map[(y-1)+my][x] == 12:
            self.s_count -= 1
            self.g_count += 1
        elif self.map[(y-1)+my][x] == 13:
            self.s_count -= 1
            self.g_count += 1

    #bottom
        self.map[(y-1)+my][x] = self.table[5][self.map[(y-1)+my][x]]



    #lower left wrap check
        if y-1 < 0 and x-1 < 0:
            mx = self.mx
            my = self.my
        elif y-1 >= 0 and x-1 < 0:
            mx = self.mx
            my = 0
        elif y-1 < 0 and x-1 >= 0:
            mx = 0
            my = self.my
        else:
            mx = 0
            my = 0

        if self.map[(y-1)+my][(x-1)+mx] == 1:
            self.w_count -= 1
            self.s_count += 1
        elif self.map[(y-1)+my][(x-1)+mx] == 12:
            self.s_count -= 1
            self.g_count += 1

    #lower left
        self.map[(y-1)+my][(x-1)+mx] = self.table[6][self.map[(y-1)+my][(x-1)+mx]]



    #left wrap check
        if x-1 < 0:
            mx = self.mx
        else:
            mx = 0

        if self.map[y][(x-1)+mx] == 1:
            self.w_count -= 1
            self.s_count += 1
        elif self.map[y][(x-1)+mx] == 6:
            self.s_count -= 1
            self.g_count += 1
        elif self.map[y][(x-1)+mx] == 10:
            self.s_count -= 1
            self.g_count += 1
        elif self.map[y][(x-1)+mx] == 12:
            self.s_count -= 1
            self.g_count += 1

    #left
        self.map[y][(x-1)+mx] = self.table[7][self.map[y][(x-1)+mx]]




if __name__ == '__main__':
    window = Prototype()
    pyglet.app.run()
-BrushTone v1.3.3: Accessible Paint Tool
-AudiMesh3D v1.0.0: Accessible 3D Model Viewer