Go 语言 Ebiten 坦克大战实现
环境准备
安装 Ebiten 图形库依赖:
go get github.com/hajimehoshi/ebiten/v2
完整代码
package main
import (
"image/color"
"math/rand"
"strconv"
"time"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
"github.com/hajimehoshi/ebiten/v2/inpututil"
)
const (
screenWidth = 640
screenHeight = 480
tankSize = 32
bulletSize = 4
wallSize = 32
)
// 方向定义
const (
Up = iota
Right
Down
Left
)
// 坦克结构
type Tank struct {
x, y float64
dx, dy float64
direction int
speed float64
isPlayer bool
shootCooldown time.Duration
lastShot time.Time
}
// 子弹结构
type Bullet struct {
x, y float64
dx, dy float64
active bool
owner *Tank
}
// 墙体结构
type Wall struct {
x, y float64
destructible bool
}
// 爆炸效果
type Explosion struct {
x, y
radius
maxRadius
active
}
Game {
player *Tank
enemies []*Tank
bullets []*Bullet
walls []*Wall
explosions []*Explosion
score
gameOver
}
*Game {
g := &Game{
bullets: ([]*Bullet, , ),
enemies: ([]*Tank, , ),
walls: ([]*Wall, ),
explosions: ([]*Explosion, ),
}
g.player = &Tank{
x: screenWidth / ,
y: screenHeight - tankSize*,
direction: Up,
speed: ,
isPlayer: ,
shootCooldown: * time.Millisecond,
}
g.createWalls()
g.spawnEnemies()
g
}
createWalls() {
x := ; x < screenWidth; x += wallSize {
g.walls = (g.walls, &Wall{x: (x), y: , destructible: })
g.walls = (g.walls, &Wall{x: (x), y: screenHeight - wallSize, destructible: })
}
y := wallSize; y < screenHeight-wallSize; y += wallSize {
g.walls = (g.walls, &Wall{x: , y: (y), destructible: })
g.walls = (g.walls, &Wall{x: screenWidth - wallSize, y: (y), destructible: })
}
rand.Seed(time.Now().UnixNano())
i := ; i < ; i++ {
x := (rand.Intn(screenWidth/wallSize)*wallSize + wallSize)
y := (rand.Intn(screenHeight/wallSize)*wallSize + wallSize)
x > g.player.x-tankSize* && x < g.player.x+tankSize* && y > g.player.y-tankSize* && y < g.player.y+tankSize* {
}
destructible := rand.Float64() <
g.walls = (g.walls, &Wall{x: x, y: y, destructible: destructible})
}
}
spawnEnemies(count ) {
i := ; i < count; i++ {
x := (rand.Intn(screenWidth/tankSize)*tankSize + tankSize*)
y := (rand.Intn(screenHeight/tankSize)*tankSize + tankSize*)
enemy := &Tank{
x: x,
y: y,
direction: Down,
speed: ,
isPlayer: ,
shootCooldown: * time.Millisecond,
}
g.enemies = (g.enemies, enemy)
}
}
shoot(t *Tank) {
now := time.Now()
now.Sub(t.lastShot) < t.shootCooldown {
}
t.lastShot = now
b := &Bullet{
owner: t,
active: ,
}
t.direction {
Up:
b.x = t.x + tankSize/ - bulletSize/
b.y = t.y
b.dy =
Right:
b.x = t.x + tankSize
b.y = t.y + tankSize/ - bulletSize/
b.dx =
Down:
b.x = t.x + tankSize/ - bulletSize/
b.y = t.y + tankSize
b.dy =
Left:
b.x = t.x
b.y = t.y + tankSize/ - bulletSize/
b.dx =
}
g.bullets = (g.bullets, b)
}
createExplosion(x, y ) {
g.explosions = (g.explosions, &Explosion{
x: x,
y: y,
radius: ,
maxRadius: ,
active: ,
})
}
{
x1 < x2+w2 && x1+w1 > x2 && y1 < y2+h2 && y1+h1 > y2
}
Update() {
g.gameOver {
inpututil.IsKeyJustPressed(ebiten.KeyR) {
*g = *NewGame()
}
}
g.handlePlayerInput()
g.updateTankPosition(g.player)
g.updateEnemies()
g.updateBullets()
g.updateExplosions()
(g.enemies) == {
g.spawnEnemies()
}
}
handlePlayerInput() {
g.player.dx =
g.player.dy =
ebiten.IsKeyPressed(ebiten.KeyUp) || ebiten.IsKeyPressed(ebiten.KeyW) {
g.player.direction = Up
g.player.dy = -g.player.speed
} ebiten.IsKeyPressed(ebiten.KeyRight) || ebiten.IsKeyPressed(ebiten.KeyD) {
g.player.direction = Right
g.player.dx = g.player.speed
} ebiten.IsKeyPressed(ebiten.KeyDown) || ebiten.IsKeyPressed(ebiten.KeyS) {
g.player.direction = Down
g.player.dy = g.player.speed
} ebiten.IsKeyPressed(ebiten.KeyLeft) || ebiten.IsKeyPressed(ebiten.KeyA) {
g.player.direction = Left
g.player.dx = -g.player.speed
}
ebiten.IsKeyPressed(ebiten.KeySpace) {
g.shoot(g.player)
}
}
updateTankPosition(t *Tank) {
oldX, oldY := t.x, t.y
t.x += t.dx
t.y += t.dy
t.x < {
t.x =
} t.x+tankSize > screenWidth {
t.x = screenWidth - tankSize
}
t.y < {
t.y =
} t.y+tankSize > screenHeight {
t.y = screenHeight - tankSize
}
collided :=
_, w := g.walls {
checkCollision(t.x, t.y, tankSize, tankSize, w.x, w.y, wallSize, wallSize) {
collided =
}
}
_, e := g.enemies {
t != e && checkCollision(t.x, t.y, tankSize, tankSize, e.x, e.y, tankSize, tankSize) {
collided =
}
}
collided {
t.x, t.y = oldX, oldY
}
}
updateEnemies() {
_, e := g.enemies {
rand.Float64() < {
e.direction = rand.Intn()
e.direction {
Up:
e.dx, e.dy = , -e.speed
Right:
e.dx, e.dy = e.speed,
Down:
e.dx, e.dy = , e.speed
Left:
e.dx, e.dy = -e.speed,
}
}
rand.Float64() < {
g.shoot(e)
}
g.updateTankPosition(e)
}
}
updateBullets() {
activeBullets := ([]*Bullet, , (g.bullets))
_, b := g.bullets {
!b.active {
}
b.x += b.dx
b.y += b.dy
b.x < || b.x > screenWidth || b.y < || b.y > screenHeight {
b.active =
}
wallHit :=
i, w := g.walls {
w.destructible && checkCollision(b.x, b.y, bulletSize, bulletSize, w.x, w.y, wallSize, wallSize) {
g.createExplosion(w.x+wallSize/, w.y+wallSize/)
g.walls = (g.walls[:i], g.walls[i+:]...)
b.active =
wallHit =
} !w.destructible && checkCollision(b.x, b.y, bulletSize, bulletSize, w.x, w.y, wallSize, wallSize) {
g.createExplosion(b.x, b.y)
b.active =
wallHit =
}
}
wallHit {
}
b.owner.isPlayer {
i, e := g.enemies {
checkCollision(b.x, b.y, bulletSize, bulletSize, e.x, e.y, tankSize, tankSize) {
g.createExplosion(e.x+tankSize/, e.y+tankSize/)
b.active =
g.enemies = (g.enemies[:i], g.enemies[i+:]...)
g.score +=
}
}
} {
checkCollision(b.x, b.y, bulletSize, bulletSize, g.player.x, g.player.y, tankSize, tankSize) {
g.createExplosion(g.player.x+tankSize/, g.player.y+tankSize/)
b.active =
g.gameOver =
}
}
b.active {
activeBullets = (activeBullets, b)
}
}
g.bullets = activeBullets
}
updateExplosions() {
activeExplosions := ([]*Explosion, , (g.explosions))
_, e := g.explosions {
!e.active {
}
e.radius +=
e.radius >= e.maxRadius {
e.active =
} {
activeExplosions = (activeExplosions, e)
}
}
g.explosions = activeExplosions
}
Draw(screen *ebiten.Image) {
screen.Fill(color.RGBA{, , , })
_, w := g.walls {
wallColor color.RGBA
w.destructible {
wallColor = color.RGBA{, , , }
} {
wallColor = color.RGBA{, , , }
}
ebitenutil.DrawRect(screen, w.x, w.y, wallSize, wallSize, wallColor)
}
ebitenutil.DrawRect(screen, g.player.x, g.player.y, tankSize, tankSize, color.RGBA{, , , })
g.drawCannon(screen, g.player)
_, e := g.enemies {
ebitenutil.DrawRect(screen, e.x, e.y, tankSize, tankSize, color.RGBA{, , , })
g.drawCannon(screen, e)
}
_, b := g.bullets {
ebitenutil.DrawRect(screen, b.x, b.y, bulletSize, bulletSize, color.White)
}
_, e := g.explosions {
r := ; r <= e.radius; r += {
alpha := ( * ( - r/e.maxRadius))
c := color.RGBA{, , , alpha}
ebitenutil.DrawCircle(screen, e.x, e.y, r, c)
}
}
ebitenutil.DebugPrint(screen, +strconv.Itoa(g.score))
g.gameOver {
ebitenutil.DebugPrintAt(screen, , screenWidth/, screenHeight/)
ebitenutil.DebugPrintAt(screen, , screenWidth/, screenHeight/+)
}
}
drawCannon(screen *ebiten.Image, t *Tank) {
cannonColor := color.RGBA{, , , }
cx, cy, cw, ch
t.direction {
Up:
cx = t.x + tankSize/ -
cy = t.y -
cw =
ch =
Right:
cx = t.x + tankSize -
cy = t.y + tankSize/ -
cw =
ch =
Down:
cx = t.x + tankSize/ -
cy = t.y + tankSize -
cw =
ch =
Left:
cx = t.x -
cy = t.y + tankSize/ -
cw =
ch =
}
ebitenutil.DrawRect(screen, cx, cy, cw, ch, cannonColor)
}
Layout(outsideWidth, outsideHeight ) (, ) {
screenWidth, screenHeight
}
{
ebiten.SetWindowSize(screenWidth, screenHeight)
ebiten.SetWindowTitle()
rand.Seed(time.Now().UnixNano())
game := NewGame()
err := ebiten.RunGame(game); err != {
(err)
}
}

