Add animated player sprite

This commit is contained in:
Trevor Slocum 2023-01-23 22:29:04 -08:00
parent aad5185f94
commit 843c254cec
7 changed files with 226 additions and 35 deletions

45
asset/asset.go Normal file
View File

@ -0,0 +1,45 @@
package asset
import (
"embed"
"image"
"github.com/hajimehoshi/ebiten/v2"
_ "image/png"
)
//go:embed image
var FS embed.FS
const tileSize = 64
var ImgPlayer = LoadImage("image/player.png")
func LoadImage(p string) *ebiten.Image {
f, err := FS.Open(p)
if err != nil {
panic(err)
}
defer f.Close()
baseImg, _, err := image.Decode(f)
if err != nil {
panic(err)
}
return ebiten.NewImageFromImage(baseImg)
}
func LoadBytes(p string) []byte {
b, err := FS.ReadFile(p)
if err != nil {
panic(err)
}
return b
}
func FrameAt(img *ebiten.Image, x int, y int) *ebiten.Image {
xPos, yPos := x*tileSize, y*tileSize
return img.SubImage(image.Rect(xPos, yPos, xPos+tileSize, yPos+tileSize)).(*ebiten.Image)
}

Binary file not shown.

BIN
asset/image/player.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -4,6 +4,10 @@ import (
"fmt"
"image"
"image/color"
"code.rocketnine.space/tslocum/boxbrawl/asset"
"github.com/hajimehoshi/ebiten/v2"
)
type PlayerAction int
@ -18,7 +22,7 @@ const (
type HitboxType int
const (
HitboxInvalid HitboxType = iota
HitboxNone HitboxType = iota
HitboxNormal
HitboxHurt
)
@ -26,22 +30,119 @@ const (
type FrameData struct {
T HitboxType
R image.Rectangle
S *ebiten.Image
}
const PlayerSize = 20
const PlayerSize = 16
const (
PlayerHeight = 48
PlayerWidth = 16
)
// stdHit returns FrameData using a standard hitbox and the provided sprite.
func stdHit(sprite *ebiten.Image) FrameData {
return FrameData{
T: HitboxNormal,
R: image.Rect(0, 0, PlayerWidth, PlayerHeight),
S: sprite,
}
}
// AllPlayerFrames defines all frame data for the game. Frames are defined in reverse order.
var AllPlayerFrames = [][][]FrameData{
{ // ActionIdle
{
{
T: HitboxNormal,
R: image.Rect(0, 0, PlayerSize, PlayerSize),
},
stdHit(asset.FrameAt(asset.ImgPlayer, 0, 0)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 1, 0)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 2, 0)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 3, 0)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 4, 0)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 5, 0)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 6, 0)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 7, 0)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 8, 0)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 9, 0)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 10, 0)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 11, 0)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 12, 0)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 13, 0)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 14, 0)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 15, 0)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 16, 0)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 17, 0)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 18, 0)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 19, 0)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 20, 0)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 21, 0)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 22, 0)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 23, 0)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 24, 0)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 25, 0)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 26, 0)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 27, 0)),
},
}, { // ActionStunned
{
// No hitboxes or hurtboxes while stunned.
FrameData{
T: HitboxNone,
R: image.Rect(0, 0, 0, 0),
S: asset.FrameAt(asset.ImgPlayer, 0, 2),
},
},
}, { // ActionBlock
{
@ -52,6 +153,7 @@ var AllPlayerFrames = [][][]FrameData{
{
T: HitboxNormal,
R: image.Rect(0, 0, PlayerSize, PlayerSize),
S: asset.FrameAt(asset.ImgPlayer, 12, 1),
},
{
T: HitboxHurt,
@ -59,34 +161,40 @@ var AllPlayerFrames = [][][]FrameData{
},
},
{
{
T: HitboxNormal,
R: image.Rect(0, 0, PlayerSize, PlayerSize),
},
stdHit(asset.FrameAt(asset.ImgPlayer, 11, 1)),
},
{
{
T: HitboxNormal,
R: image.Rect(0, 0, PlayerSize, PlayerSize),
},
stdHit(asset.FrameAt(asset.ImgPlayer, 10, 1)),
},
{
{
T: HitboxNormal,
R: image.Rect(0, 0, PlayerSize, PlayerSize),
},
stdHit(asset.FrameAt(asset.ImgPlayer, 9, 1)),
},
{
{
T: HitboxNormal,
R: image.Rect(0, 0, PlayerSize, PlayerSize),
},
stdHit(asset.FrameAt(asset.ImgPlayer, 8, 1)),
},
{
{
T: HitboxNormal,
R: image.Rect(0, 0, PlayerSize, PlayerSize),
},
stdHit(asset.FrameAt(asset.ImgPlayer, 7, 1)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 6, 1)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 5, 1)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 4, 1)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 3, 1)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 2, 1)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 1, 1)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 0, 1)),
},
},
}
@ -94,6 +202,10 @@ var AllPlayerFrames = [][][]FrameData{
func FrameDataForActionTick(a PlayerAction, tick int) []FrameData {
actionFrames := AllPlayerFrames[a]
if tick-1 >= len(actionFrames) {
// Handle
if a == ActionStunned {
return AllPlayerFrames[ActionStunned][0]
}
return nil
}
return actionFrames[tick-1]

View File

@ -20,6 +20,10 @@ import (
"github.com/hajimehoshi/ebiten/v2/inpututil"
)
const (
playerSpawnGap = 50
)
type Game struct {
Players []component.Player
}
@ -31,17 +35,17 @@ func NewGame() (*Game, error) {
PlayerNum: 1,
Color: color.RGBA{255, 0, 0, 255},
Action: component.ActionIdle,
ActionTicksLeft: 1,
X: 50,
Y: 50,
ActionTicksLeft: len(component.AllPlayerFrames[component.ActionIdle]),
X: -playerSpawnGap / 2,
Y: 0,
}
player2 := component.Player{
PlayerNum: 2,
Color: color.RGBA{0, 0, 255, 255},
Action: component.ActionIdle,
ActionTicksLeft: 1,
X: 150,
Y: 50,
ActionTicksLeft: len(component.AllPlayerFrames[component.ActionIdle]),
X: playerSpawnGap / 2,
Y: 0,
}
g := &Game{
@ -315,7 +319,7 @@ func (g *Game) UpdateByInputs(inputs []InputBits) {
// Stun the opponent.
opponent.Action = component.ActionStunned
opponent.ActionTicksLeft = 7
opponent.ActionTicksLeft = 14
}
}
}

View File

@ -37,6 +37,8 @@ func (s *MapSystem) Draw(e gohan.Entity, screen *ebiten.Image) error {
return nil
}
screen.Fill(color.RGBA{0, 100, 100, 255})
groundColor := color.RGBA{50, 50, 50, 255}
r := image.Rect(-world.ScreenWidth*2, -world.ScreenHeight, world.ScreenWidth*2, 0)

View File

@ -38,12 +38,40 @@ func (s *PlayerSystem) Draw(e gohan.Entity, screen *ebiten.Image) error {
}
size := 20
var p *component.Player
var p, o *component.Player
for i := 0; i < 2; i++ {
if i == 0 {
p = &world.Player1
o = &world.Player2
} else {
p = &world.Player2
o = &world.Player1
}
var sprite *ebiten.Image
frameData := component.FrameDataForActionTick(p.Action, p.ActionTicksLeft)
for _, frame := range frameData {
if frame.S == nil {
continue
}
sprite = frame.S
break
}
if sprite != nil {
x, y := world.GameCoordsToScreen(p.X-24, p.Y+64)
op := &ebiten.DrawImageOptions{}
if component.PlayerOnRightSide(*p, *o) {
op.GeoM.Scale(-1, 1)
op.GeoM.Translate(64, 0)
}
op.GeoM.Translate(float64(x), float64(y))
op.ColorM.ScaleWithColor(p.Color)
screen.DrawImage(sprite, op)
continue // TODO
}
r := image.Rect(int(p.X), int(p.Y), int(p.X)+size, int(p.Y)+size)