Skip to content

Instantly share code, notes, and snippets.

@clindsey
Created December 1, 2016 03:13
Show Gist options
  • Select an option

  • Save clindsey/42147e23cff3ed97c4e59accd43291bf to your computer and use it in GitHub Desktop.

Select an option

Save clindsey/42147e23cff3ed97c4e59accd43291bf to your computer and use it in GitHub Desktop.
weltFlow
<canvas id="canvas" width="500" height="500"></canvas>
<ul>
<li>move the mouse around the field to generate flow fields leading to that point</li>
<li>the system is optimized by breaking the map into many smaller sectors to generate the flow fields then stitching them together</li>
</ul>
seed = 19870910
rng = new Chance seed
options = {
width: 5
height: 5
sector:
width: 5
height: 5
tile:
width: 20
height: 20
}
options.grid = {
width: options.sector.width * options.tile.width
height: options.sector.height * options.tile.height
}
config = {}
config.generators =
world:
options:
spritesheetOffset: 0
maxHeight: 7
# world size
worldChunkWidth: options.width
worldChunkHeight: options.height
# zoom level
chunkTileWidth: options.sector.width
chunkTileHeight: options.sector.height
config.worldTileWidth = config.generators.world.options.worldChunkWidth * config.generators.world.options.chunkTileWidth
config.worldTileHeight = config.generators.world.options.worldChunkHeight * config.generators.world.options.chunkTileHeight
config.generators.world.options.worldTileWidth = config.worldTileWidth
config.generators.world.options.worldTileHeight = config.worldTileHeight
class FlowField
neighborDirections: [
[ 0, -1]
[ 1, 0]
[ 0, 1]
[-1, 0]
#[ 1, -1]
#[ 1, 1]
#[-1, 1]
#[-1, -1]
]
constructor: (@x, @y, @gridData) ->
callback: (node) =>
return unless node.data.open
node.visited = true
node.iteration = 1
@openNodes.push node
directionFlow: (vX, vY, costCallback, flowCallback) ->
@reset()
@openNodes = []
{width, height} = options.sector
lX = if vX is -1 then 0 else width - 1
lY = if vY is -1 then 0 else height - 1
offsetX = options.sector.width * @x
offsetY = options.sector.height * @y
if vX is - 1 or vX is 1
for y in [0...height]
node = @gridData.getNode lX + offsetX, y + offsetY
@callback node
if vY is -1 or vY is 1
for x in [0...width]
node = @gridData.getNode x + offsetX, lY + offsetY
@callback node
@costMap 1, costCallback
@flowMap flowCallback
if vX is -1 or vX is 1
for y in [0...height]
node = @gridData.getNode lX + offsetX, y + offsetY
continue unless node.data.open
vector = @neighborVector node, vX, 0
flowCallback lX, y, vector
if vY is -1 or vY is 1
for x in [0...width]
node = @gridData.getNode x + offsetX, lY + offsetY
continue unless node.data.open
vector = @neighborVector node, 0, vY
flowCallback x, lY, vector
neighborVector: (node, vX, vY) -> # bug here, sometimes vectors point towards eachother
neighborNode = @gridData.getNode node.x + vX, node.y + vY
if neighborNode.data.open
vector = {x: vX, y: vY}
else
offsetX = options.sector.width * @x
offsetY = options.sector.height * @y
{width, height} = options.sector
neighborNodes = @collectNeighbors node, (n) ->
rX = n.x - offsetX
rY = n.y - offsetY
return false unless rX < width and rY < height
return false unless rX >= 0 and rY >= 0
n.iteration >= 0
for n in neighborNodes
if n.data.open
vector = {
x: n.x - node.x
y: n.y - node.y
}
break
vector
process: (x, y, costCallback, flowCallback) ->
@reset()
offsetX = options.sector.width * @x
offsetY = options.sector.height * @y
@openNodes = [@gridData.getNode x + offsetX, y + offsetY]
@costMap 0, costCallback
@flowMap flowCallback
flowMap: (flowCallback) ->
{width, height} = options.sector
offsetX = options.sector.width * @x
offsetY = options.sector.height * @y
for y in [0...height]
for x in [0...width]
node = @gridData.getNode x + offsetX, y + offsetY
continue unless node.data.open
continue unless node.visited
neighbors = @collectNeighbors node, (n) ->
rX = n.x - offsetX
rY = n.y - offsetY
return false unless rX < width and rY < height
return false unless rX >= 0 and rY >= 0
n.iteration >= 0
lowestNode = neighbors.pop()
neighbors.map (n) ->
return unless n.data.open
return unless n.iteration?
lowestNode = n if lowestNode.iteration > n.iteration
continue unless lowestNode
vector = {
x: lowestNode.x - node.x
y: lowestNode.y - node.y
}
vector = {x: 0, y: 0} if node.iteration is 0
flowCallback x, y, vector
reset: ->
offsetX = options.sector.width * @x
offsetY = options.sector.height * @y
{width, height} = options.sector
for y in [0...height]
for x in [0...width]
node = @gridData.getNode x + offsetX, y + offsetY
node.visited = false
floodCallback: (node) -> # doesn't feel right to have this here
return false unless node.data.open
return false if node.visited
node.visited = true
true
costMap: (iteration, costCallback) ->
newNodes = []
while node = @openNodes.pop()
node.visited = true
node.iteration = iteration
{x, y} = node
costCallback x, y
nodes = @collectNeighbors node, @floodCallback
newNodes = newNodes.concat nodes
@openNodes = @openNodes.concat newNodes
if @openNodes.length
@costMap iteration + 1, costCallback
collectNeighbors: ({x, y}, checkCallback) ->
{width, height} = options.sector
nodes = []
for [dX, dY] in @neighborDirections
tX = x + dX
tY = y + dY
node = @gridData.getNode tX, tY
continue unless node
continue unless checkCallback node
nodes.push node
nodes
class FlowNode
constructor: (@x, @y, @iteration, @data = {}) ->
class WorldData
constructor: (mapData) ->
@buildData mapData
buildData: (mapData) ->
@width = mapData[0].length
@height = mapData.length
@nodes = []
for y in [0...@height]
@nodes.push []
for x in [0...@width]
node = new FlowNode x, y, -Infinity, { open: mapData[y][x] > 1 }
@nodes[y][x] = node
getNode: (x, y) ->
unless @nodes[y]? and @nodes[y][x]?
return node = new FlowNode x, y, -Infinity, { open: false }
@nodes[y][x]
class SectorTile
directionLookup: {
'1_0': 0
'2_1': 90
'1_2': 180
'0_1': 270
'2_0': 45
'2_2': 135
'0_2': 225
'0_0': 315
'1_1': 0
}
constructor: (x, y, @node) ->
@el = new createjs.Container
@el.x = x
@el.y = y
@render()
render: ->
@drawBackground()
@drawArrow()
drawBackground: ->
return if @node.data.open
g = new createjs.Graphics
backgroundEl = new createjs.Shape g
g.f('#880000').dr 0, 0, options.tile.width, options.tile.height
@el.addChild backgroundEl
drawArrow: ->
g = new createjs.Graphics
@arrowEl = new createjs.Shape g
width = options.tile.width * 0.25
height = options.tile.height * 0.75
x = (options.tile.width - width) / 2
y = (options.tile.height - height) / 2
g.lf(['#333333', '#dedede'], [0, 1], 0, 0, 0, height).dr x, y, width, height
@arrowEl.regX = options.tile.width / 2
@arrowEl.regY = options.tile.height / 2
@arrowEl.x = options.tile.width / 2
@arrowEl.y = options.tile.height / 2
#@arrowEl.visible = false
@el.addChild @arrowEl
setVector: ({x, y}) ->
return unless @node.data.open
return if x is 0 and y is 0
@arrowEl.visible = true
@arrowEl.rotation = @directionLookup["#{x + 1}_#{y + 1}"]
clearVector: ->
return if @node.data.open
@arrowEl.visible = false
class SectorView
constructor: (@worldData, @x, @y) ->
@el = new createjs.Container
@render()
@flowField = new FlowField @x, @y, @worldData
reset: ->
for row, y in @sectorTiles
for sectorTile, x in row
sectorTile.clearVector()
markCost: ->
markFlow: (x, y, vector) =>
@sectorTiles[y][x].setVector vector
render: ->
@drawTiles()
@el.cache 0, 0, options.grid.width, options.grid.height
drawTiles: ->
@sectorTiles = []
{width, height} = options.sector
for y in [0...height]
@sectorTiles[y] = []
for x in [0...width]
pX = x * options.tile.width
pY = y * options.tile.height
wX = @x * options.sector.width + x
wY = @y * options.sector.height + y
node = @worldData.getNode wX, wY
sectorTile = new SectorTile pX, pY, node
@el.addChild sectorTile.el
@sectorTiles[y][x] = sectorTile
process: (x, y) ->
@vX = @vY = undefined
return unless @sectorTiles[y][x].node.data.open
@flowField.process x, y, @markCost, @markFlow
@sectorTiles[y][x].arrowEl.visible = false
@el.updateCache()
directionFlow: (vX, vY) ->
return if vX is @vX and vY is @vY
@vX = vX
@vY = vY
@flowField.directionFlow vX, vY, @markCost, @markFlow
@el.updateCache()
class GridView
constructor: (@width, @height, @worldData) ->
@el = new createjs.Container
@addSectors()
@addEventListeners()
addSectors: ->
@sectors = []
for y in [0...options.height]
@sectors[y] = []
for x in [0...options.width]
pX = x * options.grid.width
pY = y * options.grid.height
sectorView = @addSector pX, pY, x, y
@el.addChild sectorView.el
@sectors[y][x] = sectorView
addSector: (x, y, sectorX, sectorY) ->
sectorView = new SectorView @worldData, sectorX, sectorY
sectorView.el.x = x
sectorView.el.y = y
sectorView
addEventListeners: ->
{width, height} = options
@cX = @cY = undefined
$('canvas').mousemove @onMouseMove
onMouseMove: ({offsetX, offsetY}) =>
sectorX = ~~(offsetX / options.grid.width)
sectorY = ~~(offsetY / options.grid.height)
mX = offsetX % options.grid.width
mY = offsetY % options.grid.height
x = ~~(mX / options.tile.width)
y = ~~(mY / options.tile.height)
if @cX isnt x or @cY isnt y
@cX = x
@cY = y
for sY in [0...options.height]
for sX in [0...options.width]
sector = @sectors[sY][sX]
sector.reset()
if sX is sectorX and sY is sectorY
sector.process x, y
else
vX = 0
vY = 0
if sectorX > sX
vX = 1
if sectorX < sX
vX = -1
if sectorY > sY
vY = 1
if sectorY < sY
vY = -1
sector.directionFlow vX, vY
class WorldGenerator
constructor: (@randomFn, @seed, @options, cacheTiles = true) ->
@tileCache = []
@chunkCache = []
@tileTypes = []
@cacheAllTiles() if cacheTiles
getStartPosition: (seed) ->
grassTiles = @tileTypes[2]
index = Math.floor @randomFn(seed) * grassTiles.length
grassTiles[index]
getFinishPosition: (seed) ->
grassTiles = @tileTypes[2]
index = Math.floor @randomFn(seed) * grassTiles.length
grassTiles[index]
getPathfindingArea: (centerX, centerY) ->
offsetX = (0 - ~~(@options.worldTileHeight / 2)) + centerX
offsetY = (0 - ~~(@options.worldTileHeight / 2)) + centerY
outputArray = []
for y in [[email protected]]
outputArray[y] = []
for x in [[email protected]]
worldX = x + offsetX
worldY = y + offsetY
cellIndex = @getCell worldX, worldY
outputArray[y][x] = cellIndex
outputArray
getAllCells: ->
@tileCache
cacheAllTiles: ->
for y in [[email protected]]
for x in [[email protected]]
chunk = @getChunk x, y
for cy in [0...chunk.length]
for cx in [0...chunk[cy].length]
vx = x * @options.chunkTileWidth + cx
vy = y * @options.chunkTileHeight + cy
cellValue = @getCell vx, vy
index = Math.floor cellValue / 2
@tileTypes[index] = [] if @tileTypes[index] is undefined
@tileTypes[index].push {x:vx, y:vy}
getCell: (worldX, worldY) ->
worldX = @clamp worldX, @options.worldTileWidth
worldY = @clamp worldY, @options.worldTileHeight
if @tileCache[worldY] and @tileCache[worldY][worldX]?
return @tileCache[worldY][worldX]
worldChunkX = Math.floor worldX / @options.chunkTileWidth
worldChunkY = Math.floor worldY / @options.chunkTileHeight
chunkX = worldX % @options.chunkTileWidth
chunkY = worldY % @options.chunkTileHeight
chunk = @getChunk worldChunkX, worldChunkY
cell = Math.floor chunk[chunkY][chunkX] * @options.maxHeight
@tileCache[worldY] = [] unless @tileCache[worldY]?
@tileCache[worldY][worldX] = cell
cell
getChunk: (worldChunkX, worldChunkY) ->
row0 = [
@chunkEdgeIndex worldChunkX - 1, worldChunkY - 1
@chunkEdgeIndex worldChunkX, worldChunkY - 1
@chunkEdgeIndex worldChunkX + 1, worldChunkY - 1
@chunkEdgeIndex worldChunkX + 2, worldChunkY - 1
]
row1 = [
@chunkEdgeIndex worldChunkX - 1, worldChunkY
@chunkEdgeIndex worldChunkX, worldChunkY
@chunkEdgeIndex worldChunkX + 1, worldChunkY
@chunkEdgeIndex worldChunkX + 2, worldChunkY
]
row2 = [
@chunkEdgeIndex worldChunkX - 1, worldChunkY + 1
@chunkEdgeIndex worldChunkX, worldChunkY + 1
@chunkEdgeIndex worldChunkX + 1, worldChunkY + 1
@chunkEdgeIndex worldChunkX + 2, worldChunkY + 1
]
row3 = [
@chunkEdgeIndex worldChunkX - 1, worldChunkY + 2
@chunkEdgeIndex worldChunkX, worldChunkY + 2
@chunkEdgeIndex worldChunkX + 1, worldChunkY + 2
@chunkEdgeIndex worldChunkX + 2, worldChunkY + 2
]
chunkTileWidth = @options.chunkTileWidth
chunkTileHeight = @options.chunkTileHeight
@bilinearInterpolate chunkTileWidth, chunkTileHeight, row0, row1, row2, row3
chunkEdgeIndex: (x, y) ->
width = @options.worldChunkWidth
height = @options.worldChunkHeight
seed = @seed
x = @clamp x, width
y = @clamp y, height
@randomFn(y * width + x + seed)
bilinearInterpolate: (width, height, row0, row1, row2, row3) ->
cells = []
for y in [0...height]
cells[y] = []
yFactor = y / height
for x in [0...width]
xFactor = x / width
i0 = @terp xFactor, row0[0], row0[1], row0[2], row0[3]
i1 = @terp xFactor, row1[0], row1[1], row1[2], row1[3]
i2 = @terp xFactor, row2[0], row2[1], row2[2], row2[3]
i3 = @terp xFactor, row3[0], row3[1], row3[2], row3[3]
cells[y][x] = @terp yFactor, i0, i1, i2, i3
cells
terp: (t, a, b, c, d) ->
val = 0.5 * (c - a + (2.0*a - 5.0*b + 4.0*c - d + (3.0*(b - c) + d - a)*t)*t)*t + b
val = Math.max 0, val
val = Math.min 1, val
val
clamp: (index, size) ->
(index + size) % size
randomFn = (seed) ->
(new Chance seed).floating {min: 0, max: 1}
createGrid = ->
generator = new WorldGenerator randomFn, seed, config.generators.world.options
worldData = new WorldData generator.tileCache
gridView = new GridView options.width, options.height, worldData
stageEl.addChild gridView.el
gridView.onMouseMove {offsetX: 0, offsetY: 0}
stageEl = new createjs.Stage 'canvas'
createjs.Ticker.setFPS 60
createjs.Ticker.addEventListener 'tick', ->
stageEl.update()
createGrid()
<script src="https://code.createjs.com/easeljs-0.8.0.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/chance/0.5.6/chance.js"></script>
<script src="//code.jquery.com/jquery-2.1.3.js"></script>
canvas {
background-color: #dedede;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment