From c0ddb0b2872a0c31dd953b9c3ad7de064013f85c Mon Sep 17 00:00:00 2001
From: faiface <faiface@ksp.sk>
Date: Sat, 22 Apr 2017 13:09:23 +0200
Subject: [PATCH] add platformer example

---
 examples/platformer/README.md      |  11 +
 examples/platformer/main.go        | 395 +++++++++++++++++++++++++++++
 examples/platformer/screenshot.png | Bin 0 -> 8504 bytes
 examples/platformer/sheet.csv      |   9 +
 examples/platformer/sheet.png      | Bin 0 -> 530 bytes
 5 files changed, 415 insertions(+)
 create mode 100644 examples/platformer/README.md
 create mode 100644 examples/platformer/main.go
 create mode 100644 examples/platformer/screenshot.png
 create mode 100644 examples/platformer/sheet.csv
 create mode 100644 examples/platformer/sheet.png

diff --git a/examples/platformer/README.md b/examples/platformer/README.md
new file mode 100644
index 0000000..c6b4e65
--- /dev/null
+++ b/examples/platformer/README.md
@@ -0,0 +1,11 @@
+# Platformer
+
+This example demostrates a way to put things together and create a simple platformer game with a
+Gopher!
+
+The pixel art feel is, other than from the pixel art spritesheet, achieved by using a 160x120px
+large off-screen canvas, drawing everything to it and then stretching it to fit the window.
+
+The Gopher spritesheet comes from excellent [Egon Elbre](https://github.com/egonelbre/gophers).
+
+[Screenshot](screenshot.png)
\ No newline at end of file
diff --git a/examples/platformer/main.go b/examples/platformer/main.go
new file mode 100644
index 0000000..fb3b31c
--- /dev/null
+++ b/examples/platformer/main.go
@@ -0,0 +1,395 @@
+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 = gp.vel.WithX(-gp.runSpeed)
+	case ctrl.X() > 0:
+		gp.vel = gp.vel.WithX(+gp.runSpeed)
+	default:
+		gp.vel = gp.vel.WithX(0)
+	}
+
+	// apply gravity and velocity
+	gp.vel += pixel.Y(gp.gravity).Scaled(dt)
+	gp.rect = gp.rect.Moved(gp.vel.Scaled(dt))
+
+	// check collisions agains 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 = gp.vel.WithY(0)
+			gp.rect = gp.rect.Moved(pixel.Y(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 = gp.vel.WithY(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 positon and direction
+	ga.sprite.Set(ga.sheet, ga.frame)
+	ga.sprite.SetMatrix(pixel.IM.
+		ScaledXY(0, pixel.V(
+			phys.rect.W()/ga.sprite.Frame().W(),
+			phys.rect.H()/ga.sprite.Frame().H(),
+		)).
+		ScaledXY(0, pixel.V(-ga.dir, 1)).
+		Moved(phys.rect.Center()),
+	)
+	ga.sprite.Draw(t)
+}
+
+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.V(0, 0)
+
+	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/64, dt))
+		cam := pixel.IM.Moved(-camPos)
+		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())
+			phys.vel = 0
+		}
+
+		// control the gopher with keys
+		ctrl := pixel.V(0, 0)
+		if win.Pressed(pixelgl.KeyLeft) {
+			ctrl -= pixel.X(1)
+		}
+		if win.Pressed(pixelgl.KeyRight) {
+			ctrl += pixel.X(1)
+		}
+		if win.JustPressed(pixelgl.KeyUp) {
+			ctrl = ctrl.WithY(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(0,
+			math.Min(
+				win.Bounds().W()/canvas.Bounds().W(),
+				win.Bounds().H()/canvas.Bounds().H(),
+			),
+		).Moved(win.Bounds().Center()))
+		canvas.Draw(win)
+		win.Update()
+	}
+}
+
+func main() {
+	pixelgl.Run(run)
+}
diff --git a/examples/platformer/screenshot.png b/examples/platformer/screenshot.png
new file mode 100644
index 0000000000000000000000000000000000000000..4b8b54be64b0ad7a2a72d3e3fe2e5c947504f4a9
GIT binary patch
literal 8504
zcmeHNdstIfw%-T}K5;Hnq4IQ`*3Vi62O}DUq*76@1*#YjkcR?AVIadJJc4=HR;eS1
z+A0e2aIhfZ5s`O*1Yc0gBl1dskOT@CAcUY09tlZiAGH0xxnHlzy)*yZJLQjka?aU%
zueE+_t>0Svd|dk6_2Al78&?4U)*e3e=`jHG;7{ET3|7F4gm?ZMc+rdAfB3io{5xk5
zgxtR$bKpdbJN}E9xKmNVAS41G9=ts|Fe*4WBKizIMyktl1i%U${`8aM@pmMA(szPj
zlIWE*^Rrd^@BOFgO1Hy@{&005-QfDFdmkU!K4rMw%2yFEXmZat&gt3uV|r`Wk8e2p
zX<^a*tH<|p9Qf}YUO7^3afIi4;p#VkEX~j}$>MvIZZrJw$)i6;OD(UpANufA<86hV
zZ8ld-AI`chB)D9UzMjCoA?DG1y>TeXt-vb_hH?FoO&zywLo9Zldi?dP<<NxWk9bvh
zVK1MrY*`RxQ}{Qx`=|<NSw*zi!dTKPjAF7@>PLQgJ0z<jTGTc!krWFA0%yB5aOIl*
z>d2u`IH1Uj^YV@~W(@m+v0wE)+U^uOEq9Wsn8aX)l5N}in50D6?5syEKZ3}D>l&=e
zc@idmf_>Xo)N1=h%PmG|H%;NwWES1><yzC-{gr`DS3Ej15cDr{zFuk67yYs&0WZFr
zWHIh2AI%M!FTo&sI~02qT99ViCppt>j5^BGlWV=JeiM8X5I@mVIyB<p=H`aS<LN_c
z_{_#R#ni6Eq0M;drV^@C*@&uNiOilyMz9`>j8w1;x#*3C5_N$Mr+9wRIP{r_^<r-N
zlF=5<u#@0;NM@w7l9M1p6dON?ksr;BZS*4a7kLrH+tuOO>_tHtn-k4;@H}V{8Dt(m
zphKcp@g&|PwgW-rFT}Yy%3|tOG9z(cA)PiPYBRsdUJFbd?m*G&_D0N3pb4r)^u!A%
z@mL7c-A?%WwtP4r|9)4Qs>$5HU^!)8)mI)o9YGgXec{5N3b8B<=Ml%_R73b~dN!G8
z9yvegR7wH?-$g?+(8F_-xhiqj!gL{RyjMzLkwtUXMJ(mGXIQ(X@1KIgii8hJX*9<N
zQyA{nEt$K5*vkQ0G~DS*uTs@kb*<%E7uwYguEpVGk1tzuZXBT(BysTf6{=T)PaHg<
zzANg^cHB$fb@rH2NLfhhD-CQa6fj|oct?%kDn_my3>9;VQE&=cG*Kp=BP;k7XP&<&
zYhV!Ij^~?q&}p=9g|Dvk)kKE~#c7mL8jY5I?uSjX8!WlByR*|hyFUB?X-I{VEv~li
zY8mSk8(K5(3wDRN3MrWRuP$%slBbx8v(@apbjs_)gpupb;&R(Inyp|VO!k%KKF<tG
z!7*0+3u{g-o8n2psNx+S#QKJddp))&f3zR}@q;w3N+I>c;e4vX3p4NwpYb80lAh8s
z*L{QYjF`d4#357R3#m8PkA&Mo?$rr6z9+P=yl-AYDLJyi*CU%T7T6tHF`l}kw$4H_
zSyJGhw?<wAL63RSY|T0;Oq}iYR<!CvFJ=`4raCgD>!{hgVV0mrbG{nCQ^qcNo@90`
zPTA@p8)K78@?38p6J<_kR0N8y5T|?$yKCDV@{xy0u8#MJi99+{N#)j;^hsiU39SSK
zLPB2w%eynrwHOyyTyi082kpJ=`tS4HGCXj&Y@gDHf}ME3R+88!?5C%fNQn!5_3F>_
z@5CIdpPuOqlL`CXE!ADEsPOSF7;Rf+15RRb*qp!zpJAWhrc5a-<VSEvEEx}X;}fa{
zBvoywkDFUDBa9HvXH+=OwwubcdWGHq+ufv|$gP;_FysWLVveSL%qa3M_8?$p<IT|t
zQT&*pwF)v819N!v>b4R;29IsHXMidn@Vlb+kM6!f!Z<EYJqaEB{&MJ(#*~UFrMiHF
z?>}Kk7T-m5BK)*+%S^p6%THK0GDr;IGvqgn+=^09#)VgS7ZVjLo4AjU7<Wtj7n_+<
zrlq>FhNQlKrixcVQq0n5#O8@!S$pb9e;iJz5R_GXA?cHMis^;WM_mkk%Ob;*u?Y@~
zQ=4(Pic>>XVNV+I+yzmVyHx$vWtb?>^Rs9Q%M(7<b@Qj|Bh<>d>J4DZ2%o;IAVT%D
zN%044cZoYGyY6fPH1l;%_BH^pNoID5vF5R(!$jrNOQ`6^WVFmv#C?Q67s!&TyXi_&
zv3EU#QEOS4mF5Sd+%kfX9w1m!PIED4u6q~mg;sdOjHuij9SWt16$cOCy1y=2Ny3mv
zlw{7egBC}_I3>lRtVHHgL|Df5L@0=h&$mEbo8wAo*20?5s#GOcO0JTYKMM0&4X(S4
zrhl%GR0-Y4j;HoZ=2j~tBb#Sk5QbS8I|AlLR4Bu)3d-wiXvjD6h7Z)=6N!GbRLX1j
zue|4ONFS&t(t3+I)f+M7MQWDpM`LllM8GEtN_@l2X5_w23vA5X9kfK4&EPdzB+UO#
z*LyWMres+z=ImG)2$l`IyUCQ<>ijG+V~1Ca7k}|m#(3H~$N10L??JBBGqH={1_{C)
z93c9WkMyAtKP{IQC*U!6VHyb}$|lxp6|Yh<-D{iY1{nes#_diJEq(#r-dCOwT|d)t
z1QjSh>NLCexP(!DpEj;g&*BM-{WWx=f-N8bP}dhsS-i`%R^e4oU7(Dqjsvo;PB-q~
zKJe@^j!^XHWI>NL7`*82#(itJ_g)sPZ1VoU-aw6|QEF(k2;T)`*$<mjyX-hcy%MFN
z{mcs*P2N(xjixAVLh|C-YC&yNh-G3+J4*VrifcE?VwKdWHlvjH&k~wwtdhu~#_kK!
z1ol<31!BO$mh9u|85YaW^JIOwAgX_YIC#N$+@Gc3%fAhyzu!>-0*Z*k?FYrJwjmrB
zJHd-NzA6I~>Au}N>RBOktIc}ID9P*7C{rEso@~lP)i{M*!<=8ZAsY=$?N-a>cH{QK
z1=m*0!tgkGqLex3FwGTu8HV?KZdl~w87Clkn%jkWu+qjLd~!P1*QctV=tEL672_&>
zPFF@qTM@=L*0=S}PSOvoZ4`bIy@XwI!pol}WkT{I5S|Vuag)b73%S7*FqPgvmpaEB
zukc6D4}3xqsoyIu_NkKdOklNatd4ZSBW2Ts{HA)YM{caB$Ov?hG7b!^kP!=XkjpXy
zWc<0{`r5}${+Vb9pETHriDTbq<;@|l*OZl5s-~X(a+5ge#h<Z&FQ}(DG+JzD1~Tbl
z+E(Q9^H%>0>Ax+3-Om5WU%w&t{~0kxx&UBl!5JZT^%pjk#c=ZSs$gO|?paD)jk#P*
z3$?3XNo_y<amu7=O0uaQFxj^S$<TL@!L4QBX;iMd=M)+WV(!&WP$O6tm=#HxxYT|g
zU^i+ZB6(%*t337U4eT)DYV%0N>k1eADePzZki0(9X7xud!rAPoXv(+Fue*TJGC*x_
zSvobmgV)~ViOq`jCXvbVz?^go^E3ObfXzltP&WJ?<m`-4SXNI4riR8SZ~J7OiBUeX
zfd`uaV@;~QA=U;HY(UTVsfsXSjDM%|_$YQrM~Ax074gHbGVmKsmySG6k6hH7xmZd#
z8imSXVQgL8AVelRG_5^@!_|(tfL!6j)-wBtS9q#2Lg;=h1-)}qz*v#X6XptV^|^L<
zEorgYr88F*Q5PPWE0(ripWBX)QL4!JbPGcRE%3|1;Usx>I>{EzyV(}As2wNv;d{9E
zqj;43T@HJ5jEt5=_%maF$f|&`y(w`6XmCT$O8>Pey@BK(coE5rh7)OjnxtctB$Z35
zzFEQS?$FL}uwXzmxK!lc!K3bgx|6Qx$-PKQxpuBoZJh60*TfZ04;PyYo=>7t<~b~(
z8hYBK1GX^EXiaX0DYNV4m(Qlt1&{A{3R6^>M32sIb)9MTN-rj9p>N%!9>}3+^4VMC
zc2a@vUxvajCy`R_n#t_C<h+bmQ~)+-8zHE?qq2Uc1N?$}U-r7-&j%!Cls_0IHQQGk
z0cz<p?L7St;QqHwLQdh>TCkbj;gumL@sQ}MHu{h43BuP1E!ljii7j9*QFTV87v-jS
zpM61Lb)x4!3K9>Zzb{C<rcp4v0yO;aQj_oA^eiU22D@I)j)cw4%a>mk5#qPxHU%_J
z$?VWC4zccb3bTU&_1aTSmgfNwC}`R4Ht|XZvwP!1)Xh440t;XJs`8mrWm`aU7f&@e
z8@Bs9xk9NA4h?ConN5q_G>$tH-{3mdRn00r4U^6VuLDeS;dcetF}?k$@hgbK_7-WS
zoLP|Z<AO3FKbp>>7^x1SJ=~s*Yx~Rt?g>UQ(alEYULt6F5^1eF+-~qE4T@1q^6Y?3
z$ArXr!Syo(lL9+Qs$M2TVhI91j?ymKym8$TM5BhD%0Ev}obn%nu?{n*e(Y&{!|z>E
z<+U)#uM{EG=}M6<m|CSNUt&=8I@G%lsxo-?kdg3q+<Z5KGjb2LDrUTr0{!+_li%LZ
z!CRKQeKS0ZKPt~OG2GLYZn1UGv4e7nOXra4W?Ns^s2l+LUo_dc6*PPkhz7bNc~Ck@
zBmcnZ_fPL{=;6+!s1k>u#sRxDu0i|`bdq)S8o$nK(p~5L<Do~__HpoT$#PvSOI|{5
z<bLpk9j?}*!2VKqtZu%d1~#2B8><RXR_ah%eyq`7_r%q&w!Jz83~bR9jRC7b1Gr`O
zqNqKdN1?2U&Q+ZWf}dlP%`|bD&2o^lDe&yy8cr}WS?=<TWe-lb={Pak4ww=zPIx)h
zRCyGlg4oM|pQ)x$`Na}&23zYe#Ukt5qUkJx+|+Q-t)mpD8f#cf1Lif=a4drUJJQYy
zdH-`QE~nA>Gamx0)-Pqe*?IY2_v)`1LmP*WzW*>}?TEm;m~+)(HHZ(_0z9xQhqV=C
zK8%_?pLmXRf391I23V0Ml3b!#fQg(m7s(Wr_Cnc>O+NX2!Swo^%e!aR^lrrA3Nhzi
zTKHoWa+WD9P+CMRVjj@(##M;+VnG8t{E<A|1%LPGHjzco%mzTU)D*s0&rVZVNs3rL
zQ=R#`0Az|ZFwWb9B_wIEcv5akFaXIPYY{YHqs^_1Y2zZ{?lty0;Fu=2!g2ykK4_~S
zs)M23%T@EK0*Wk&Z8o)WoBrbO0M<y8kE{?}uv8CRMTS5xnIK#orK9SXQ=RLSscPof
zb^tJ1R--pzibb}2N0_~3cR1-{nx=BT1h9b>vg6NzshyC2k0riuzHl^NccX*_kqac+
z#iMoz{^q9NLhenIsa?GMdt}^F6ZMWG)o+MtacKQN&gx5F0~&f{Y5tj9tr`a^EMKtm
z(9Z0){XAUoq*=QYz^|O3tPp`(RLuEiVH}*vME(~5S)!$m|AfSUxs(C5t}5xnYl#K+
R9?0H@54e6>vOnO<zXN??y?Fot

literal 0
HcmV?d00001

diff --git a/examples/platformer/sheet.csv b/examples/platformer/sheet.csv
new file mode 100644
index 0000000..159846d
--- /dev/null
+++ b/examples/platformer/sheet.csv
@@ -0,0 +1,9 @@
+Front,0,0
+FrontBlink,1,1
+LookUp,2,2
+Left,3,7
+LeftRight,4,6
+LeftBlink,7,7
+Walk,8,15
+Run,16,23
+Jump,24,26
\ No newline at end of file
diff --git a/examples/platformer/sheet.png b/examples/platformer/sheet.png
new file mode 100644
index 0000000000000000000000000000000000000000..8be1b97b1cffdcfbcb783840c5f8c2fe092a7e03
GIT binary patch
literal 530
zcmV+t0`2{YP)<h;3K|Lk000e1NJLTq00Be*000gM0{{R3V1SiM0000FP)t-s|Ns90
z00960|6<VfvwcToNsci9000GaQchC<|NsC09MC3G00054Nkl<ZScTP=fo{Vf3`7n1
z|G#-54sd{v5@qSMNTtuo`LI1QRpj4aDV3$?9Zmjn<aoccqyh1sd0WT}eVXgDT{EK(
z)AV}B6D2%IP6p%sf?ngX{+MEnR}gE!dqfT9e$>n0-#^HZ*FHIfd_S+l#=s#Yld%R8
zZ*q$<mvD$j38v3*1=O?CKrZuddKq%zfx)?U&EtJcHaezvY1)0erXi9hcerbEgWvFf
zHpFux=gNl&gTsM-b%^AoQ%`o%ssEGL8Sn4BdiZL8^L3Wc_L3CZzQhwXn9KZA(M!wK
zBT!?l|D;1Lf-!7-mr0Yia~WH=)=i3rOaD0KF8xvOah~C^;~Cm?eg0^)Vk~shDB(F*
zD<@#9vj!axo?7-zsRUQ9_WnH=Pi^}10d6&H?6XcA-{q^DnY%)PmE>N3ROo)~v*^p$
z=*a5ndz*W|6JOz-Ccn+Ptrr=8xibEPX5hr>QeVzJ_CgK^_6vF~3EGm?_XpNmWix%7
z^`@q=R*h=WV`HLqF%|90(+duw`pcxpES*e5)jn@=gDC(Flyp}wuEMeU4ZU;y0QQg{
Uk<x}3WdHyG07*qoM6N<$f={*quK)l5

literal 0
HcmV?d00001

-- 
GitLab