Add unit system

This commit is contained in:
Trevor Slocum 2022-11-18 13:19:58 -08:00
parent 868cfe9aa3
commit dbf2d00b37
16 changed files with 200 additions and 67 deletions

3
component/building.go Normal file
View File

@ -0,0 +1,3 @@
package component
type Building struct{}

6
component/health.go Normal file
View File

@ -0,0 +1,6 @@
package component
type Health struct {
Current int
Max int
}

5
component/movable.go Normal file
View File

@ -0,0 +1,5 @@
package component
type Movable struct {
Speed float64
}

6
component/position.go Normal file
View File

@ -0,0 +1,6 @@
package component
type Position struct {
X float64
Y float64
}

7
component/sprite.go Normal file
View File

@ -0,0 +1,7 @@
package component
import "github.com/hajimehoshi/ebiten/v2"
type Sprite struct {
Image *ebiten.Image
}

13
component/unit.go Normal file
View File

@ -0,0 +1,13 @@
package component
type Unit struct {
Type UnitType
}
type UnitType int
const (
UnitTypeNone UnitType = iota
UnitTypeWorker
UnitTypeBuildingBarracks
)

48
entity/unit.go Normal file
View File

@ -0,0 +1,48 @@
package entity
import (
"image/color"
"code.rocketnine.space/tslocum/commandeuropa/component"
"code.rocketnine.space/tslocum/gohan"
"github.com/hajimehoshi/ebiten/v2"
)
func isBuildingUnit(unitType component.UnitType) bool {
return unitType == component.UnitTypeBuildingBarracks
}
func unitSprite(unitType component.UnitType) *ebiten.Image {
var img *ebiten.Image
if isBuildingUnit(unitType) {
img = ebiten.NewImage(16, 16)
img.Fill(color.RGBA{0, 255, 0, 255})
} else {
img = ebiten.NewImage(16, 16)
img.Fill(color.RGBA{0, 0, 255, 255})
}
return img
}
func NewUnit(unitType component.UnitType, x float64, y float64) gohan.Entity {
hitPoints := 10
e := gohan.NewEntity()
e.AddComponent(&component.Position{
X: x,
Y: y,
})
if isBuildingUnit(unitType) {
e.AddComponent(&component.Building{})
} else {
e.AddComponent(&component.Movable{})
}
e.AddComponent(&component.Sprite{
Image: unitSprite(unitType),
})
e.AddComponent(&component.Health{
Current: hitPoints,
Max: hitPoints,
})
return e
}

View File

@ -7,6 +7,8 @@ import (
"os"
"time"
"code.rocketnine.space/tslocum/commandeuropa/entity"
"code.rocketnine.space/tslocum/commandeuropa/component"
"code.rocketnine.space/tslocum/commandeuropa/system"
"code.rocketnine.space/tslocum/commandeuropa/world"
@ -27,6 +29,7 @@ func NewGame() (*Game, error) {
gohan.AddSystem(&system.HandleInput{})
gohan.AddSystem(&system.RenderEnvironment{})
gohan.AddSystem(&system.RenderUnit{})
gohan.AddSystem(&system.RenderDebug{})
// Create singleton entity for systems that run one time each tick.
@ -35,18 +38,21 @@ func NewGame() (*Game, error) {
world.Map = world.NewGameMap(time.Now().UnixNano())
_ = entity.NewUnit(component.UnitTypeWorker, 4, 4)
_ = entity.NewUnit(component.UnitTypeBuildingBarracks, 8, 8)
return g, nil
}
func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
// Handle window resize.
if outsideWidth != world.ScreenWidth || outsideHeight != world.ScreenHeight {
world.ScreenWidth, world.ScreenHeight = outsideWidth, outsideHeight
if float64(outsideWidth) != world.ScreenWidth || float64(outsideHeight) != world.ScreenHeight {
world.ScreenWidth, world.ScreenHeight = float64(outsideWidth), float64(outsideHeight)
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)))
world.MinCamScale = math.Ceil(math.Max(minZoomWidth, minZoomHeight))
}
return outsideWidth, outsideHeight

View File

@ -14,7 +14,7 @@ import (
func main() {
ebiten.SetWindowTitle("Command Europa")
ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled)
ebiten.SetWindowSize(world.ScreenWidth, world.ScreenHeight)
ebiten.SetWindowSize(int(world.ScreenWidth), int(world.ScreenHeight))
ebiten.SetFullscreen(true)
ebiten.SetWindowClosingHandled(true)
ebiten.SetFPSMode(ebiten.FPSModeVsyncOn)

View File

@ -15,7 +15,8 @@ type HandleInput struct {
func (r *HandleInput) Update(e gohan.Entity) error {
// Update cursor position.
world.CursorX, world.CursorY = ebiten.CursorPosition()
cx, cy := ebiten.CursorPosition()
world.CursorX, world.CursorY = float64(cx), float64(cy)
// Clamp cursor position.
if world.CursorX < 0 {
@ -42,7 +43,7 @@ func (r *HandleInput) Update(e gohan.Entity) error {
world.CamScale = world.MinCamScale
}
panDistance := 4
panDistance := float64(4)
// Pan with keyboard.
if ebiten.IsKeyPressed(ebiten.KeyLeft) {
@ -86,7 +87,7 @@ func (r *HandleInput) Update(e gohan.Entity) error {
world.CamY = (world.TileSize * world.MapSize) - halfHeight
}
if inpututil.IsKeyJustPressed(ebiten.KeyV) && ebiten.IsKeyPressed(ebiten.KeyShift) {
if inpututil.IsKeyJustPressed(ebiten.KeyV) && ebiten.IsKeyPressed(ebiten.KeyControl) {
world.Debug++
if world.Debug > 2 {
world.Debug = 0
@ -102,7 +103,7 @@ func (r *HandleInput) Update(e gohan.Entity) error {
world.MapPath = nil
for _, p := range path {
t := p.(world.Tile)
world.MapPath = append(world.MapPath, [2]int{t.X, t.Y})
world.MapPath = append(world.MapPath, [2]float64{t.X, t.Y})
}
}

View File

@ -2,7 +2,6 @@ package system
import (
"fmt"
"image"
"image/color"
"code.rocketnine.space/tslocum/commandeuropa/component"
@ -45,7 +44,7 @@ func (s *RenderDebug) Draw(e gohan.Entity, screen *ebiten.Image) error {
for y := 0; y < world.MapSize; y++ {
for ox := 0; ox < 4; ox++ {
for oy := 0; oy < 4; oy++ {
px, py := x*4+ox, y*4+oy
px, py := float64(x*4+ox), float64(y*4+oy)
fillColor := color.RGBA{255, 0, 0, 255}
if world.Map[world.PathTileIndex(px, py)].Walkable {
fillColor = color.RGBA{0, 255, 0, 255}
@ -58,7 +57,7 @@ func (s *RenderDebug) Draw(e gohan.Entity, screen *ebiten.Image) error {
continue
}
screen.SubImage(image.Rect(drawX, drawY, drawX+4*world.CamScale, drawY+4*world.CamScale)).(*ebiten.Image).Fill(fillColor)
screen.SubImage(floatRect(drawX, drawY, drawX+4*world.CamScale, drawY+4*world.CamScale)).(*ebiten.Image).Fill(fillColor)
}
}
}
@ -75,7 +74,7 @@ func (s *RenderDebug) Draw(e gohan.Entity, screen *ebiten.Image) error {
}
fillColor := color.RGBA{255, 255, 255, 255}
screen.SubImage(image.Rect(drawX, drawY, drawX+4*world.CamScale, drawY+4*world.CamScale)).(*ebiten.Image).Fill(fillColor)
screen.SubImage(floatRect(drawX, drawY, drawX+4*world.CamScale, drawY+4*world.CamScale)).(*ebiten.Image).Fill(fillColor)
}
}
}

View File

@ -30,8 +30,8 @@ func (r *RenderEnvironment) Draw(e gohan.Entity, screen *ebiten.Image) error {
r.Initialize()
}
for x := 0; x < world.MapSize; x++ {
for y := 0; y < world.MapSize; y++ {
for x := 0.0; x < world.MapSize; x++ {
for y := 0.0; y < world.MapSize; y++ {
i := world.TileIndex(x, y)
t := world.Map[i]
@ -46,11 +46,11 @@ func (r *RenderEnvironment) Draw(e gohan.Entity, screen *ebiten.Image) error {
continue
}
r.renderTile(t, x, y, screen)
renderSprite(screen, t.Sprite, x, y, r.op)
}
}
highlightX, highlightY := -1, -1
highlightX, highlightY := -1.0, -1.0
if highlightX >= 0 && highlightY >= 0 && highlightX < world.MapSize && highlightY < world.MapSize {
boxX, boxY := world.LevelCoordinatesToScreen(highlightX, highlightY)
right := boxX + world.TileSize*world.CamScale
@ -58,35 +58,39 @@ func (r *RenderEnvironment) Draw(e gohan.Entity, screen *ebiten.Image) error {
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)
screen.SubImage(floatRect(boxX, boxY, right, boxY+outerSize)).(*ebiten.Image).Fill(outerColor)
screen.SubImage(floatRect(boxX, bottom-outerSize, right, bottom)).(*ebiten.Image).Fill(outerColor)
screen.SubImage(floatRect(boxX, boxY, boxX+outerSize, bottom)).(*ebiten.Image).Fill(outerColor)
screen.SubImage(floatRect(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)
screen.SubImage(floatRect(boxX+innerPadding, boxY+innerPadding, right-innerPadding, boxY+innerPadding+innerSize)).(*ebiten.Image).Fill(innerColor)
screen.SubImage(floatRect(boxX+innerPadding, bottom-innerPadding-innerSize, right-innerPadding, bottom-innerPadding)).(*ebiten.Image).Fill(innerColor)
screen.SubImage(floatRect(boxX+innerPadding, boxY+innerPadding, boxX+innerPadding+innerSize, bottom-innerPadding)).(*ebiten.Image).Fill(innerColor)
screen.SubImage(floatRect(right-innerPadding-innerSize, boxY+innerPadding, right-innerPadding, bottom-innerPadding)).(*ebiten.Image).Fill(innerColor)
}
return nil
}
func (r *RenderEnvironment) renderTile(t world.Tile, x int, y int, target *ebiten.Image) int {
r.op.GeoM.Reset()
func renderSprite(target *ebiten.Image, img *ebiten.Image, x float64, y float64, op *ebiten.DrawImageOptions) int {
op.GeoM.Reset()
// Move to current position.
r.op.GeoM.Translate(float64(x*world.TileSize), float64(y*world.TileSize))
op.GeoM.Translate(x*world.TileSize, y*world.TileSize)
// Translate camera position.
r.op.GeoM.Translate(float64(-world.CamX), float64(-world.CamY))
op.GeoM.Translate(float64(-world.CamX), float64(-world.CamY))
// Zoom.
r.op.GeoM.Scale(float64(world.CamScale), float64(world.CamScale))
op.GeoM.Scale(float64(world.CamScale), float64(world.CamScale))
// Center.
r.op.GeoM.Translate(float64(world.ScreenWidth/2), float64(world.ScreenHeight/2))
op.GeoM.Translate(float64(world.ScreenWidth/2), float64(world.ScreenHeight/2))
target.DrawImage(t.Sprite, r.op)
target.DrawImage(img, op)
return 1
}
func floatRect(x0, y0, x1, y1 float64) image.Rectangle {
return image.Rect(int(x0), int(y0), int(x1), int(y1))
}

32
system/renderunit.go Normal file
View File

@ -0,0 +1,32 @@
package system
import (
"code.rocketnine.space/tslocum/commandeuropa/component"
"code.rocketnine.space/tslocum/gohan"
"github.com/hajimehoshi/ebiten/v2"
)
type RenderUnit struct {
Position *component.Position
Sprite *component.Sprite
op *ebiten.DrawImageOptions
initialized bool
}
func (r *RenderUnit) Initialize() {
r.op = &ebiten.DrawImageOptions{}
}
func (r *RenderUnit) Update(e gohan.Entity) error {
return gohan.ErrUnregister
}
func (r *RenderUnit) Draw(e gohan.Entity, screen *ebiten.Image) error {
if !r.initialized {
r.Initialize()
}
renderSprite(screen, r.Sprite.Image, r.Position.X, r.Position.Y, r.op)
return nil
}

View File

@ -44,7 +44,7 @@ func NewGameMap(seed int64) []Tile {
tiles := make([]Tile, numTiles)
{
tx, ty := 0, 0
tx, ty := float64(0), float64(0)
for i := range tiles {
tiles[i].X, tiles[i].Y = tx, ty
tiles[i].Walkable = true
@ -57,14 +57,14 @@ func NewGameMap(seed int64) []Tile {
}
}
for x := 0; x < MapSize; x++ {
for y := 0; y < MapSize; y++ {
for x := 0.0; x < MapSize; x++ {
for y := 0.0; y < MapSize; y++ {
index := TileIndex(x, y)
tiles[index].Sprite = lightBlueTiles[r.Intn(len(lightBlueTiles))]
}
}
setWalkable := func(tx, ty int, walkable bool) {
setWalkable := func(tx, ty float64, walkable bool) {
tileIndexes := PathTilesAtMapTile(tx, ty)
for _, index := range tileIndexes {
tiles[index].Walkable = walkable
@ -73,11 +73,11 @@ func NewGameMap(seed int64) []Tile {
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++ {
bx, by := float64(r.Intn(132)-4), float64(r.Intn(132)-4)
bSizeX := float64(r.Intn(TileSize) + 7)
bSizeY := float64(r.Intn(TileSize) + 7)
for offsetX := 0.0; offsetX < bSizeX; offsetX++ {
for offsetY := 0.0; offsetY < bSizeY; offsetY++ {
tx, ty := bx+offsetX, by+offsetY
index := TileIndex(tx, ty)
if index == -1 {
@ -91,11 +91,11 @@ func NewGameMap(seed int64) []Tile {
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++ {
bx, by := float64(r.Intn(132)-4), float64(r.Intn(132)-4)
bSizeX := float64(r.Intn(12) + 7)
bSizeY := float64(r.Intn(12) + 7)
for offsetX := 0.0; offsetX < bSizeX; offsetX++ {
for offsetY := 0.0; offsetY < bSizeY; offsetY++ {
tx, ty := bx+offsetX, by+offsetY
index := TileIndex(tx, ty)
if index == -1 {
@ -109,12 +109,12 @@ func NewGameMap(seed int64) []Tile {
numStreaks := r.Intn(3) + 5
for i := 0; i < numStreaks; i++ {
bx, by := r.Intn(132)-4, r.Intn(132)-4
bx, by := float64(r.Intn(132)-4), float64(r.Intn(132)-4)
vertical := r.Intn(2) == 0
bSize := rand.Intn(14) + 32
for offset := 0; offset < bSize; offset++ {
var tx, ty int
bSize := float64(rand.Intn(14) + 32)
for offset := 0.0; offset < bSize; offset++ {
var tx, ty float64
if vertical {
tx, ty = bx, by+offset
} else {
@ -132,25 +132,25 @@ func NewGameMap(seed int64) []Tile {
return tiles
}
func PathTileIndex(x, y int) int {
func PathTileIndex(x, y float64) int {
if x < 0 || y < 0 || x >= MapSize*tileDivisions || y >= MapSize*tileDivisions {
return -1
}
return y*MapSize*tileDivisions + x
return int(y*MapSize*tileDivisions) + int(x)
}
func TileIndex(x, y int) int {
func TileIndex(x, y float64) int {
if x < 0 || y < 0 || x >= MapSize || y >= MapSize {
return -1
}
return y*tileDivisions*MapSize*tileDivisions + x*tileDivisions
return int(y*tileDivisions*MapSize*tileDivisions) + int(x*tileDivisions)
}
func ValidXY(x, y int) bool {
return x >= 0 && y >= 0 && x < MapSize && y < MapSize
}
func PathTilesAtMapTile(tx, ty int) []int {
func PathTilesAtMapTile(tx, ty float64) []int {
pi := TileIndex(tx, ty)
tiles := make([]int, 16)

View File

@ -8,8 +8,8 @@ import (
const TileSize = 16
type Tile struct {
X int
Y int
X float64
Y float64
Walkable bool
Sprite *ebiten.Image

View File

@ -9,18 +9,21 @@ import (
const TPS = 144
var (
ScreenWidth, ScreenHeight = 800, 600
ScreenWidth float64 = 800
ScreenHeight float64 = 600
CamX, CamY = 0, 0
CamX float64
CamY float64
CamScale = 1
MinCamScale = 1
CamScale float64 = 1
MinCamScale float64 = 1
CursorX, CursorY = 0, 0
CursorX float64
CursorY float64
Map []Tile
MapPath [][2]int
MapPath [][2]float64
Debug int
@ -37,15 +40,15 @@ func init() {
blankSprite.Fill(color.RGBA{0, 0, 255, 255})
}
func LevelCoordinatesToScreen(x, y int) (int, int) {
func LevelCoordinatesToScreen(x, y float64) (float64, float64) {
return (x*TileSize-CamX)*CamScale + ScreenWidth/2, (y*TileSize-CamY)*CamScale + ScreenHeight/2
}
func PathCoordinatesToScreen(x, y int) (int, int) {
func PathCoordinatesToScreen(x, y float64) (float64, float64) {
return (x*tileDivisions-CamX)*CamScale + ScreenWidth/2, (y*tileDivisions-CamY)*CamScale + ScreenHeight/2
}
func PixelCoordinatesOffScreen(x, y int) bool {
func PixelCoordinatesOffScreen(x, y float64) bool {
padding := TileSize * CamScale
left, right := x, x+TileSize*CamScale
top, bottom := y, y+TileSize*CamScale
@ -53,10 +56,10 @@ func PixelCoordinatesOffScreen(x, y int) bool {
(right < -padding || right > ScreenWidth+padding) || (bottom < -padding || bottom > ScreenHeight+padding)
}
func ScreenCoordinatesToLevel(x, y int) (int, int) {
func ScreenCoordinatesToLevel(x, y float64) (float64, float64) {
return (((x - ScreenWidth/2) / CamScale) + CamX) / TileSize, (((y - ScreenHeight/2) / CamScale) + CamY) / TileSize
}
func ScreenCoordinatesToPath(x, y int) (int, int) {
func ScreenCoordinatesToPath(x, y float64) (float64, float64) {
return (((x - ScreenWidth/2) / CamScale) + CamX) / tileDivisions, (((y - ScreenHeight/2) / CamScale) + CamY) / tileDivisions
}