From 60fc3a544ccbdf5461c9b47eb8c2a46498a98b39 Mon Sep 17 00:00:00 2001 From: Trevor Slocum Date: Wed, 9 Nov 2022 23:36:00 -0800 Subject: [PATCH] Procedurally generate terrain --- game/game.go | 17 +++---- go.mod | 2 +- go.sum | 4 +- main.go | 1 + system/handleinput.go | 47 +++++++++++++----- system/renderenvironment.go | 34 +++++++++++-- world/map.go | 97 ++++++++++++++++++++++++++++++++++++- world/world.go | 17 +++++-- 8 files changed, 184 insertions(+), 35 deletions(-) diff --git a/game/game.go b/game/game.go index ebaefc2..b24c7b7 100644 --- a/game/game.go +++ b/game/game.go @@ -5,6 +5,7 @@ import ( "log" "math" "os" + "time" "code.rocketnine.space/tslocum/commandeuropa/component" "code.rocketnine.space/tslocum/commandeuropa/system" @@ -18,10 +19,10 @@ type Game struct{} func NewGame() (*Game, error) { g := &Game{} - redSprite := ebiten.NewImage(16, 16) + redSprite := ebiten.NewImage(world.TileSize, world.TileSize) redSprite.Fill(color.RGBA{255, 0, 0, 255}) - greenSprite := ebiten.NewImage(16, 16) + greenSprite := ebiten.NewImage(world.TileSize, world.TileSize) greenSprite.Fill(color.RGBA{0, 255, 0, 255}) gohan.AddSystem(&system.HandleInput{}) @@ -32,11 +33,7 @@ func NewGame() (*Game, error) { once := gohan.NewEntity() once.AddComponent(&component.Once{}) - // TODO populate map for testing - world.Map = world.NewGameMap(128, 128) - world.Map[128].Sprite = greenSprite - world.Map[129].Sprite = redSprite - world.Map[130].Sprite = greenSprite + world.Map = world.NewGameMap(time.Now().UnixNano(), world.MapSize, world.MapSize) return g, nil } @@ -46,9 +43,9 @@ func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeigh if outsideWidth != world.ScreenWidth || outsideHeight != world.ScreenHeight { world.ScreenWidth, world.ScreenHeight = outsideWidth, outsideHeight - mapSize := float64(16 * 128) - minZoomWidth := float64(world.ScreenWidth) / mapSize - minZoomHeight := float64(world.ScreenHeight) / mapSize + fullSize := float64(world.TileSize * world.MapSize) + minZoomWidth := float64(world.ScreenWidth) / fullSize + minZoomHeight := float64(world.ScreenHeight) / fullSize world.MinCamScale = int(math.Ceil(math.Max(minZoomWidth, minZoomHeight))) } diff --git a/go.mod b/go.mod index daeda41..788885d 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,6 @@ require ( github.com/jezek/xgb v1.1.0 // indirect golang.org/x/exp/shiny v0.0.0-20221109205753-fc8884afc316 // indirect golang.org/x/image v0.1.0 // indirect - golang.org/x/mobile v0.0.0-20221020085226-b36e6246172e // indirect + golang.org/x/mobile v0.0.0-20221110043201-43a038452099 // indirect golang.org/x/sys v0.2.0 // indirect ) diff --git a/go.sum b/go.sum index 37f0956..81e3604 100644 --- a/go.sum +++ b/go.sum @@ -40,8 +40,8 @@ golang.org/x/image v0.1.0/go.mod h1:iyPr49SD/G/TBxYVB/9RRtGUT5eNbo2u4NamWeQcD5c= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mobile v0.0.0-20220722155234-aaac322e2105/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ= -golang.org/x/mobile v0.0.0-20221020085226-b36e6246172e h1:zSgtO19fpg781xknwqiQPmOHaASr6E7ZVlTseLd9Fx4= -golang.org/x/mobile v0.0.0-20221020085226-b36e6246172e/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY= +golang.org/x/mobile v0.0.0-20221110043201-43a038452099 h1:aIu0lKmfdgtn2uTj7JI2oN4TUrQvgB+wzTPO23bCKt8= +golang.org/x/mobile v0.0.0-20221110043201-43a038452099/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= diff --git a/main.go b/main.go index 231b0c6..a4672ff 100644 --- a/main.go +++ b/main.go @@ -20,6 +20,7 @@ func main() { ebiten.SetFPSMode(ebiten.FPSModeVsyncOn) ebiten.SetTPS(world.TPS) ebiten.SetRunnableOnUnfocused(true) + ebiten.SetCursorShape(ebiten.CursorShapeCrosshair) parseFlags() diff --git a/system/handleinput.go b/system/handleinput.go index 563c223..cb229c0 100644 --- a/system/handleinput.go +++ b/system/handleinput.go @@ -5,6 +5,7 @@ import ( "code.rocketnine.space/tslocum/commandeuropa/world" "code.rocketnine.space/tslocum/gohan" "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/inpututil" ) type HandleInput struct { @@ -12,6 +13,21 @@ type HandleInput struct { } func (r *HandleInput) Update(e gohan.Entity) error { + // Update cursor position. + world.CursorX, world.CursorY = ebiten.CursorPosition() + + // Clamp cursor position. + if world.CursorX < 0 { + world.CursorX = 0 + } else if world.CursorX > world.ScreenWidth-1 { + world.CursorX = world.ScreenWidth - 1 + } + if world.CursorY < 0 { + world.CursorY = 0 + } else if world.CursorY > world.ScreenHeight-1 { + world.CursorY = world.ScreenHeight - 1 + } + // Zoom with mouse. _, scroll := ebiten.Wheel() if scroll < 0 { @@ -43,31 +59,40 @@ func (r *HandleInput) Update(e gohan.Entity) error { // Pan with mouse. const panArea = 7 - x, y := ebiten.CursorPosition() - if x <= panArea { + if world.CursorX <= panArea { world.CamX -= panDistance - } else if x >= world.ScreenWidth-panArea { + } else if world.CursorX >= world.ScreenWidth-panArea { world.CamX += panDistance } - if y <= panArea { + if world.CursorY <= panArea { world.CamY -= panDistance - } else if y >= world.ScreenHeight-panArea { + } else if world.CursorY >= world.ScreenHeight-panArea { world.CamY += panDistance } // Clamp camera position. - halfWidth := (world.ScreenWidth / 2) / world.CamScale - halfHeight := (world.ScreenHeight / 2) / world.CamScale + padding := world.TileSize * world.CamScale / 2 + halfWidth := (world.ScreenWidth/2)/world.CamScale - padding + halfHeight := (world.ScreenHeight/2)/world.CamScale - padding if world.CamX < halfWidth { world.CamX = halfWidth - } else if world.CamX > (16*128)-halfWidth { - world.CamX = (16 * 128) - halfWidth + } else if world.CamX > (world.TileSize*world.MapSize)-halfWidth { + world.CamX = (world.TileSize * world.MapSize) - halfWidth } if world.CamY < halfHeight { world.CamY = halfHeight - } else if world.CamY > (16*128)-halfHeight { - world.CamY = (16 * 128) - halfHeight + } else if world.CamY > (world.TileSize*world.MapSize)-halfHeight { + world.CamY = (world.TileSize * world.MapSize) - halfHeight } + + if inpututil.IsKeyJustPressed(ebiten.KeyV) && ebiten.IsKeyPressed(ebiten.KeyShift) { + if world.Debug == 0 { + world.Debug = 1 + } else { + world.Debug = 0 + } + } + return nil } diff --git a/system/renderenvironment.go b/system/renderenvironment.go index f08871f..4c186f5 100644 --- a/system/renderenvironment.go +++ b/system/renderenvironment.go @@ -1,6 +1,9 @@ package system import ( + "image" + "image/color" + "code.rocketnine.space/tslocum/commandeuropa/component" "code.rocketnine.space/tslocum/commandeuropa/world" "code.rocketnine.space/tslocum/gohan" @@ -30,7 +33,7 @@ func (r *RenderEnvironment) Draw(e gohan.Entity, screen *ebiten.Image) error { x, y := -1, 0 for _, t := range world.Map { x++ - if x == 128 { + if x == world.MapSize { y++ x = 0 } @@ -38,8 +41,8 @@ func (r *RenderEnvironment) Draw(e gohan.Entity, screen *ebiten.Image) error { drawX, drawY := world.LevelCoordinatesToScreen(x, y) // Skip drawing off-screen tiles. - padding := world.TileWidth * world.CamScale - width, height := world.TileWidth*world.CamScale, world.TileWidth*world.CamScale + padding := world.TileSize * world.CamScale + width, height := world.TileSize*world.CamScale, world.TileSize*world.CamScale left := drawX right := drawX + width top := drawY @@ -52,6 +55,29 @@ func (r *RenderEnvironment) Draw(e gohan.Entity, screen *ebiten.Image) error { r.renderTile(t, x, y, screen) } + + highlightX, highlightY := -1, -1 + if highlightX >= 0 && highlightY >= 0 && highlightX < world.MapSize && highlightY < world.MapSize { + boxX, boxY := world.LevelCoordinatesToScreen(highlightX, highlightY) + right := boxX + world.TileSize*world.CamScale + bottom := boxY + world.TileSize*world.CamScale + + outerColor := color.RGBA{0, 0, 0, 255} + outerSize := 3 * world.CamScale + screen.SubImage(image.Rect(boxX, boxY, right, boxY+outerSize)).(*ebiten.Image).Fill(outerColor) + screen.SubImage(image.Rect(boxX, bottom-outerSize, right, bottom)).(*ebiten.Image).Fill(outerColor) + screen.SubImage(image.Rect(boxX, boxY, boxX+outerSize, bottom)).(*ebiten.Image).Fill(outerColor) + screen.SubImage(image.Rect(right-outerSize, boxY, right, bottom)).(*ebiten.Image).Fill(outerColor) + + innerColor := color.RGBA{255, 255, 255, 255} + innerPadding := 1 * world.CamScale + innerSize := 1 * world.CamScale + screen.SubImage(image.Rect(boxX+innerPadding, boxY+innerPadding, right-innerPadding, boxY+innerPadding+innerSize)).(*ebiten.Image).Fill(innerColor) + screen.SubImage(image.Rect(boxX+innerPadding, bottom-innerPadding-innerSize, right-innerPadding, bottom-innerPadding)).(*ebiten.Image).Fill(innerColor) + screen.SubImage(image.Rect(boxX+innerPadding, boxY+innerPadding, boxX+innerPadding+innerSize, bottom-innerPadding)).(*ebiten.Image).Fill(innerColor) + screen.SubImage(image.Rect(right-innerPadding-innerSize, boxY+innerPadding, right-innerPadding, bottom-innerPadding)).(*ebiten.Image).Fill(innerColor) + } + return nil } @@ -59,7 +85,7 @@ func (r *RenderEnvironment) renderTile(t world.MapTile, x int, y int, target *eb r.op.GeoM.Reset() // Move to current position. - r.op.GeoM.Translate(float64(x*16), float64(y*16)) + r.op.GeoM.Translate(float64(x*world.TileSize), float64(y*world.TileSize)) // Translate camera position. r.op.GeoM.Translate(float64(-world.CamX), float64(-world.CamY)) // Zoom. diff --git a/world/map.go b/world/map.go index 5194608..387416c 100644 --- a/world/map.go +++ b/world/map.go @@ -1,17 +1,110 @@ package world import ( + "image/color" + "math/rand" + "github.com/hajimehoshi/ebiten/v2" ) +func colorTile(r, g, b uint8) *ebiten.Image { + img := ebiten.NewImage(TileSize, TileSize) + img.Fill(color.RGBA{r, g, b, 255}) + return img +} + +var ( + lightBlueTile1 = colorTile(174, 208, 218) + lightBlueTile2 = colorTile(203, 234, 229) + lightBlueTile3 = colorTile(154, 202, 206) + lightBlueTiles = []*ebiten.Image{lightBlueTile1, lightBlueTile1, lightBlueTile2, lightBlueTile2, lightBlueTile2, lightBlueTile2, lightBlueTile2, lightBlueTile2, lightBlueTile2, lightBlueTile3} + + mediumBlueTile1 = colorTile(0, 38, 117) + mediumBlueTile2 = colorTile(9, 69, 131) + mediumBlueTile3 = colorTile(25, 71, 148) + mediumBlueTiles = []*ebiten.Image{mediumBlueTile1, mediumBlueTile2, mediumBlueTile3} + + darkBlueTile1 = colorTile(0, 9, 88) + darkBlueTile2 = colorTile(2, 3, 67) + darkBlueTile3 = colorTile(0, 0, 72) + darkBlueTiles = []*ebiten.Image{darkBlueTile1, darkBlueTile1, darkBlueTile2, darkBlueTile3} + + redTile1 = colorTile(109, 5, 0) + redTile2 = colorTile(135, 13, 8) + redTile3 = colorTile(118, 0, 6) + redTiles = []*ebiten.Image{redTile1, redTile1, redTile2, redTile3} +) + type MapTile struct { Sprite *ebiten.Image } -func NewGameMap(w, h int) []MapTile { +func NewGameMap(seed int64, w, h int) []MapTile { + r := rand.New(rand.NewSource(seed)) + tiles := make([]MapTile, w*h) for i := range tiles { - tiles[i].Sprite = blankSprite + tiles[i].Sprite = lightBlueTiles[r.Intn(len(lightBlueTiles))] } + + numMediumBlue := r.Intn(10) + 7 + for i := 0; i < numMediumBlue; i++ { + bx, by := r.Intn(132)-4, r.Intn(132)-4 + bSizeX := r.Intn(TileSize) + 7 + bSizeY := r.Intn(TileSize) + 7 + for offsetX := 0; offsetX < bSizeX; offsetX++ { + for offsetY := 0; offsetY < bSizeY; offsetY++ { + index := TileIndex(bx+offsetX, by+offsetY) + if index == -1 { + continue + } + tiles[index].Sprite = mediumBlueTiles[r.Intn(len(mediumBlueTiles))] + } + } + } + + numDarkBlue := r.Intn(7) + 4 + for i := 0; i < numDarkBlue; i++ { + bx, by := r.Intn(132)-4, r.Intn(132)-4 + bSizeX := r.Intn(12) + 7 + bSizeY := r.Intn(12) + 7 + for offsetX := 0; offsetX < bSizeX; offsetX++ { + for offsetY := 0; offsetY < bSizeY; offsetY++ { + index := TileIndex(bx+offsetX, by+offsetY) + if index == -1 { + continue + } + tiles[index].Sprite = darkBlueTiles[r.Intn(len(darkBlueTiles))] + } + } + } + + numStreaks := r.Intn(3) + 5 + for i := 0; i < numStreaks; i++ { + bx, by := r.Intn(132)-4, r.Intn(132)-4 + vertical := r.Intn(2) == 0 + + bSize := rand.Intn(14) + 32 + for offset := 0; offset < bSize; offset++ { + var index int + if vertical { + index = TileIndex(bx, by+offset) + } else { + index = TileIndex(bx+offset, by) + } + if index == -1 { + continue + } + tiles[index].Sprite = redTiles[r.Intn(len(redTiles))] + } + } + return tiles } + +func TileIndex(x, y int) int { + if x < 0 || y < 0 || x >= MapSize || y >= MapSize { + return -1 + } + return y*MapSize + x +} diff --git a/world/world.go b/world/world.go index bdd3b97..7e0890b 100644 --- a/world/world.go +++ b/world/world.go @@ -8,17 +8,20 @@ import ( const TPS = 144 -const TileWidth = 16 +const TileSize = 16 + +const MapSize = 128 var ( ScreenWidth, ScreenHeight = 800, 600 CamX, CamY = 0, 0 - CamScale = 1 - + CamScale = 1 MinCamScale = 1 + CursorX, CursorY = 0, 0 + Map []MapTile Debug int @@ -30,12 +33,16 @@ var ( DisableEsc bool ) -var blankSprite = ebiten.NewImage(16, 16) +var blankSprite = ebiten.NewImage(TileSize, TileSize) func init() { blankSprite.Fill(color.RGBA{0, 0, 255, 255}) } func LevelCoordinatesToScreen(x, y int) (int, int) { - return (x*16-CamX)*CamScale + ScreenWidth/2, (y*16-CamY)*CamScale + ScreenHeight/2 + return (x*TileSize-CamX)*CamScale + ScreenWidth/2, (y*TileSize-CamY)*CamScale + ScreenHeight/2 +} + +func ScreenCoordinatesToLevel(x, y int) (int, int) { + return (((x - ScreenWidth/2) / CamScale) + CamX) / TileSize, (((y - ScreenHeight/2) / CamScale) + CamY) / TileSize }