Procedurally generate terrain

This commit is contained in:
Trevor Slocum 2022-11-09 23:36:00 -08:00
parent 250b027164
commit 60fc3a544c
8 changed files with 184 additions and 35 deletions

View file

@ -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)))
}

2
go.mod
View file

@ -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
)

4
go.sum
View file

@ -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=

View file

@ -20,6 +20,7 @@ func main() {
ebiten.SetFPSMode(ebiten.FPSModeVsyncOn)
ebiten.SetTPS(world.TPS)
ebiten.SetRunnableOnUnfocused(true)
ebiten.SetCursorShape(ebiten.CursorShapeCrosshair)
parseFlags()

View file

@ -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
}

View file

@ -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.

View file

@ -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
}

View file

@ -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
}