Procedurally generate terrain
This commit is contained in:
parent
250b027164
commit
60fc3a544c
8 changed files with 184 additions and 35 deletions
17
game/game.go
17
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)))
|
||||
}
|
||||
|
||||
|
|
2
go.mod
2
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
|
||||
)
|
||||
|
|
4
go.sum
4
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=
|
||||
|
|
1
main.go
1
main.go
|
@ -20,6 +20,7 @@ func main() {
|
|||
ebiten.SetFPSMode(ebiten.FPSModeVsyncOn)
|
||||
ebiten.SetTPS(world.TPS)
|
||||
ebiten.SetRunnableOnUnfocused(true)
|
||||
ebiten.SetCursorShape(ebiten.CursorShapeCrosshair)
|
||||
|
||||
parseFlags()
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
97
world/map.go
97
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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue