395 lines
7.9 KiB
Go
395 lines
7.9 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/csv"
|
|
"image"
|
|
"image/color"
|
|
"io"
|
|
"math"
|
|
"math/rand"
|
|
"os"
|
|
"strconv"
|
|
"time"
|
|
|
|
_ "image/png"
|
|
|
|
"github.com/faiface/pixel"
|
|
"github.com/faiface/pixel/imdraw"
|
|
"github.com/faiface/pixel/pixelgl"
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/image/colornames"
|
|
)
|
|
|
|
func loadAnimationSheet(sheetPath, descPath string, frameWidth float64) (sheet pixel.Picture, anims map[string][]pixel.Rect, err error) {
|
|
// total hack, nicely format the error at the end, so I don't have to type it every time
|
|
defer func() {
|
|
if err != nil {
|
|
err = errors.Wrap(err, "error loading animation sheet")
|
|
}
|
|
}()
|
|
|
|
// open and load the spritesheet
|
|
sheetFile, err := os.Open(sheetPath)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
defer sheetFile.Close()
|
|
sheetImg, _, err := image.Decode(sheetFile)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
sheet = pixel.PictureDataFromImage(sheetImg)
|
|
|
|
// create a slice of frames inside the spritesheet
|
|
var frames []pixel.Rect
|
|
for x := 0.0; x+frameWidth <= sheet.Bounds().Max.X; x += frameWidth {
|
|
frames = append(frames, pixel.R(
|
|
x,
|
|
0,
|
|
x+frameWidth,
|
|
sheet.Bounds().H(),
|
|
))
|
|
}
|
|
|
|
descFile, err := os.Open(descPath)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
defer descFile.Close()
|
|
|
|
anims = make(map[string][]pixel.Rect)
|
|
|
|
// load the animation information, name and interval inside the spritesheet
|
|
desc := csv.NewReader(descFile)
|
|
for {
|
|
anim, err := desc.Read()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
name := anim[0]
|
|
start, _ := strconv.Atoi(anim[1])
|
|
end, _ := strconv.Atoi(anim[2])
|
|
|
|
anims[name] = frames[start : end+1]
|
|
}
|
|
|
|
return sheet, anims, nil
|
|
}
|
|
|
|
type platform struct {
|
|
rect pixel.Rect
|
|
color color.Color
|
|
}
|
|
|
|
func (p *platform) draw(imd *imdraw.IMDraw) {
|
|
imd.Color = p.color
|
|
imd.Push(p.rect.Min, p.rect.Max)
|
|
imd.Rectangle(0)
|
|
}
|
|
|
|
type gopherPhys struct {
|
|
gravity float64
|
|
runSpeed float64
|
|
jumpSpeed float64
|
|
|
|
rect pixel.Rect
|
|
vel pixel.Vec
|
|
ground bool
|
|
}
|
|
|
|
func (gp *gopherPhys) update(dt float64, ctrl pixel.Vec, platforms []platform) {
|
|
// apply controls
|
|
switch {
|
|
case ctrl.X < 0:
|
|
gp.vel.X = -gp.runSpeed
|
|
case ctrl.X > 0:
|
|
gp.vel.X = +gp.runSpeed
|
|
default:
|
|
gp.vel.X = 0
|
|
}
|
|
|
|
// apply gravity and velocity
|
|
gp.vel.Y += gp.gravity * dt
|
|
gp.rect = gp.rect.Moved(gp.vel.Scaled(dt))
|
|
|
|
// check collisions against each platform
|
|
gp.ground = false
|
|
if gp.vel.Y <= 0 {
|
|
for _, p := range platforms {
|
|
if gp.rect.Max.X <= p.rect.Min.X || gp.rect.Min.X >= p.rect.Max.X {
|
|
continue
|
|
}
|
|
if gp.rect.Min.Y > p.rect.Max.Y || gp.rect.Min.Y < p.rect.Max.Y+gp.vel.Y*dt {
|
|
continue
|
|
}
|
|
gp.vel.Y = 0
|
|
gp.rect = gp.rect.Moved(pixel.V(0, p.rect.Max.Y-gp.rect.Min.Y))
|
|
gp.ground = true
|
|
}
|
|
}
|
|
|
|
// jump if on the ground and the player wants to jump
|
|
if gp.ground && ctrl.Y > 0 {
|
|
gp.vel.Y = gp.jumpSpeed
|
|
}
|
|
}
|
|
|
|
type animState int
|
|
|
|
const (
|
|
idle animState = iota
|
|
running
|
|
jumping
|
|
)
|
|
|
|
type gopherAnim struct {
|
|
sheet pixel.Picture
|
|
anims map[string][]pixel.Rect
|
|
rate float64
|
|
|
|
state animState
|
|
counter float64
|
|
dir float64
|
|
|
|
frame pixel.Rect
|
|
|
|
sprite *pixel.Sprite
|
|
}
|
|
|
|
func (ga *gopherAnim) update(dt float64, phys *gopherPhys) {
|
|
ga.counter += dt
|
|
|
|
// determine the new animation state
|
|
var newState animState
|
|
switch {
|
|
case !phys.ground:
|
|
newState = jumping
|
|
case phys.vel.Len() == 0:
|
|
newState = idle
|
|
case phys.vel.Len() > 0:
|
|
newState = running
|
|
}
|
|
|
|
// reset the time counter if the state changed
|
|
if ga.state != newState {
|
|
ga.state = newState
|
|
ga.counter = 0
|
|
}
|
|
|
|
// determine the correct animation frame
|
|
switch ga.state {
|
|
case idle:
|
|
ga.frame = ga.anims["Front"][0]
|
|
case running:
|
|
i := int(math.Floor(ga.counter / ga.rate))
|
|
ga.frame = ga.anims["Run"][i%len(ga.anims["Run"])]
|
|
case jumping:
|
|
speed := phys.vel.Y
|
|
i := int((-speed/phys.jumpSpeed + 1) / 2 * float64(len(ga.anims["Jump"])))
|
|
if i < 0 {
|
|
i = 0
|
|
}
|
|
if i >= len(ga.anims["Jump"]) {
|
|
i = len(ga.anims["Jump"]) - 1
|
|
}
|
|
ga.frame = ga.anims["Jump"][i]
|
|
}
|
|
|
|
// set the facing direction of the gopher
|
|
if phys.vel.X != 0 {
|
|
if phys.vel.X > 0 {
|
|
ga.dir = +1
|
|
} else {
|
|
ga.dir = -1
|
|
}
|
|
}
|
|
}
|
|
|
|
func (ga *gopherAnim) draw(t pixel.Target, phys *gopherPhys) {
|
|
if ga.sprite == nil {
|
|
ga.sprite = pixel.NewSprite(nil, pixel.Rect{})
|
|
}
|
|
// draw the correct frame with the correct position and direction
|
|
ga.sprite.Set(ga.sheet, ga.frame)
|
|
ga.sprite.Draw(t, pixel.IM.
|
|
ScaledXY(pixel.ZV, pixel.V(
|
|
phys.rect.W()/ga.sprite.Frame().W(),
|
|
phys.rect.H()/ga.sprite.Frame().H(),
|
|
)).
|
|
ScaledXY(pixel.ZV, pixel.V(-ga.dir, 1)).
|
|
Moved(phys.rect.Center()),
|
|
)
|
|
}
|
|
|
|
type goal struct {
|
|
pos pixel.Vec
|
|
radius float64
|
|
step float64
|
|
|
|
counter float64
|
|
cols [5]pixel.RGBA
|
|
}
|
|
|
|
func (g *goal) update(dt float64) {
|
|
g.counter += dt
|
|
for g.counter > g.step {
|
|
g.counter -= g.step
|
|
for i := len(g.cols) - 2; i >= 0; i-- {
|
|
g.cols[i+1] = g.cols[i]
|
|
}
|
|
g.cols[0] = randomNiceColor()
|
|
}
|
|
}
|
|
|
|
func (g *goal) draw(imd *imdraw.IMDraw) {
|
|
for i := len(g.cols) - 1; i >= 0; i-- {
|
|
imd.Color = g.cols[i]
|
|
imd.Push(g.pos)
|
|
imd.Circle(float64(i+1)*g.radius/float64(len(g.cols)), 0)
|
|
}
|
|
}
|
|
|
|
func randomNiceColor() pixel.RGBA {
|
|
again:
|
|
r := rand.Float64()
|
|
g := rand.Float64()
|
|
b := rand.Float64()
|
|
len := math.Sqrt(r*r + g*g + b*b)
|
|
if len == 0 {
|
|
goto again
|
|
}
|
|
return pixel.RGB(r/len, g/len, b/len)
|
|
}
|
|
|
|
func run() {
|
|
rand.Seed(time.Now().UnixNano())
|
|
|
|
sheet, anims, err := loadAnimationSheet("sheet.png", "sheet.csv", 12)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
cfg := pixelgl.WindowConfig{
|
|
Title: "Platformer",
|
|
Bounds: pixel.R(0, 0, 1024, 768),
|
|
VSync: true,
|
|
}
|
|
win, err := pixelgl.NewWindow(cfg)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
phys := &gopherPhys{
|
|
gravity: -512,
|
|
runSpeed: 64,
|
|
jumpSpeed: 192,
|
|
rect: pixel.R(-6, -7, 6, 7),
|
|
}
|
|
|
|
anim := &gopherAnim{
|
|
sheet: sheet,
|
|
anims: anims,
|
|
rate: 1.0 / 10,
|
|
dir: +1,
|
|
}
|
|
|
|
// hardcoded level
|
|
platforms := []platform{
|
|
{rect: pixel.R(-50, -34, 50, -32)},
|
|
{rect: pixel.R(20, 0, 70, 2)},
|
|
{rect: pixel.R(-100, 10, -50, 12)},
|
|
{rect: pixel.R(120, -22, 140, -20)},
|
|
{rect: pixel.R(120, -72, 140, -70)},
|
|
{rect: pixel.R(120, -122, 140, -120)},
|
|
{rect: pixel.R(-100, -152, 100, -150)},
|
|
{rect: pixel.R(-150, -127, -140, -125)},
|
|
{rect: pixel.R(-180, -97, -170, -95)},
|
|
{rect: pixel.R(-150, -67, -140, -65)},
|
|
{rect: pixel.R(-180, -37, -170, -35)},
|
|
{rect: pixel.R(-150, -7, -140, -5)},
|
|
}
|
|
for i := range platforms {
|
|
platforms[i].color = randomNiceColor()
|
|
}
|
|
|
|
gol := &goal{
|
|
pos: pixel.V(-75, 40),
|
|
radius: 18,
|
|
step: 1.0 / 7,
|
|
}
|
|
|
|
canvas := pixelgl.NewCanvas(pixel.R(-160/2, -120/2, 160/2, 120/2))
|
|
imd := imdraw.New(sheet)
|
|
imd.Precision = 32
|
|
|
|
camPos := pixel.ZV
|
|
|
|
last := time.Now()
|
|
for !win.Closed() {
|
|
dt := time.Since(last).Seconds()
|
|
last = time.Now()
|
|
|
|
// lerp the camera position towards the gopher
|
|
camPos = pixel.Lerp(camPos, phys.rect.Center(), 1-math.Pow(1.0/128, dt))
|
|
cam := pixel.IM.Moved(camPos.Scaled(-1))
|
|
canvas.SetMatrix(cam)
|
|
|
|
// slow motion with tab
|
|
if win.Pressed(pixelgl.KeyTab) {
|
|
dt /= 8
|
|
}
|
|
|
|
// restart the level on pressing enter
|
|
if win.JustPressed(pixelgl.KeyEnter) {
|
|
phys.rect = phys.rect.Moved(phys.rect.Center().Scaled(-1))
|
|
phys.vel = pixel.ZV
|
|
}
|
|
|
|
// control the gopher with keys
|
|
ctrl := pixel.ZV
|
|
if win.Pressed(pixelgl.KeyLeft) {
|
|
ctrl.X--
|
|
}
|
|
if win.Pressed(pixelgl.KeyRight) {
|
|
ctrl.X++
|
|
}
|
|
if win.JustPressed(pixelgl.KeyUp) {
|
|
ctrl.Y = 1
|
|
}
|
|
|
|
// update the physics and animation
|
|
phys.update(dt, ctrl, platforms)
|
|
gol.update(dt)
|
|
anim.update(dt, phys)
|
|
|
|
// draw the scene to the canvas using IMDraw
|
|
canvas.Clear(colornames.Black)
|
|
imd.Clear()
|
|
for _, p := range platforms {
|
|
p.draw(imd)
|
|
}
|
|
gol.draw(imd)
|
|
anim.draw(imd, phys)
|
|
imd.Draw(canvas)
|
|
|
|
// stretch the canvas to the window
|
|
win.Clear(colornames.White)
|
|
win.SetMatrix(pixel.IM.Scaled(pixel.ZV,
|
|
math.Min(
|
|
win.Bounds().W()/canvas.Bounds().W(),
|
|
win.Bounds().H()/canvas.Bounds().H(),
|
|
),
|
|
).Moved(win.Bounds().Center()))
|
|
canvas.Draw(win, pixel.IM.Moved(canvas.Bounds().Center()))
|
|
win.Update()
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
pixelgl.Run(run)
|
|
}
|