diff --git a/demo/keyframe/assets/runningcat.png b/demo/keyframe/assets/runningcat.png new file mode 100644 index 0000000..b9ee97b Binary files /dev/null and b/demo/keyframe/assets/runningcat.png differ diff --git a/demo/keyframe/keyframe.go b/demo/keyframe/keyframe.go new file mode 100644 index 0000000..d7cf921 --- /dev/null +++ b/demo/keyframe/keyframe.go @@ -0,0 +1,60 @@ +package main + +import ( + "github.com/DeKugelschieber/go-game" +) + +const ( + assets_dir = "src/github.com/DeKugelschieber/go-game/demo/keyframe/assets" +) + +type Game struct{} + +func (g *Game) Setup() { + err := goga.LoadResFromFolder(assets_dir) + + if err != nil { + panic(err) + } + + tex, err := goga.GetTex("runningcat.png") + + if err != nil { + panic(err) + } + + // create a keyframe set + set := goga.NewKeyframeSet() + set.Add(goga.NewKeyframe(goga.Vec2{0, 0}, goga.Vec2{0.5, 0.25})) + set.Add(goga.NewKeyframe(goga.Vec2{0.5, 0}, goga.Vec2{1, 0.25})) + set.Add(goga.NewKeyframe(goga.Vec2{0, 0.25}, goga.Vec2{0.5, 0.5})) + set.Add(goga.NewKeyframe(goga.Vec2{0.5, 0.25}, goga.Vec2{1, 0.5})) + set.Add(goga.NewKeyframe(goga.Vec2{0, 0.5}, goga.Vec2{0.5, 0.75})) + set.Add(goga.NewKeyframe(goga.Vec2{0.5, 0.5}, goga.Vec2{1, 0.75})) + set.Add(goga.NewKeyframe(goga.Vec2{0, 0.75}, goga.Vec2{0.5, 1})) + set.Add(goga.NewKeyframe(goga.Vec2{0.5, 0.75}, goga.Vec2{1, 1})) + + // create a new animated sprite + sprite := goga.NewAnimatedSprite(tex, set, 512, 256) + sprite.KeyframeAnimation = goga.NewKeyframeAnimation(0, 7, true, 20) + + // add to renderer + renderer, ok := goga.GetSystemByName("keyframeRenderer").(*goga.KeyframeRenderer) + + if !ok { + panic("Could not find renderer") + } + + renderer.Add(sprite.Actor, sprite.Pos2D, sprite.Tex, sprite.KeyframeSet, sprite.KeyframeAnimation) +} + +func (g *Game) Update(delta float64) {} + +func main() { + game := Game{} + options := goga.RunOptions{ClearColor: goga.Vec4{0, 0, 0, 0}, + Resizable: true, + SetViewportOnResize: true, + ExitOnClose: true} + goga.Run(&game, &options) +} diff --git a/game.go b/game.go index de256eb..1490733 100644 --- a/game.go +++ b/game.go @@ -309,6 +309,7 @@ func initGoga(width, height int) { AddSystem(NewSpriteRenderer(nil, nil, false)) AddSystem(NewModelRenderer(nil, nil, false)) AddSystem(NewCulling2D(0, 0, width, height)) + AddSystem(NewKeyframeRenderer(nil, nil)) } func cleanup() { diff --git a/keyframe.go b/keyframe.go new file mode 100644 index 0000000..81bf121 --- /dev/null +++ b/keyframe.go @@ -0,0 +1,250 @@ +package goga + +import ( + "github.com/go-gl/gl/v4.5-core/gl" +) + +const ( + keyframe_sprite_renderer_name = "keyframeRenderer" +) + +// A single keyframe within a keyframe set. +type Keyframe struct { + texCoord *VBO + Min, Max Vec2 +} + +// Creates a new single keyframe with texture VBO. +func NewKeyframe(min, max Vec2) *Keyframe { + keyframe := &Keyframe{Min: min, Max: max} + + texData := make([]float32, 8) + texData = []float32{float32(min.X), + float32(max.Y), + float32(max.X), + float32(max.Y), + float32(min.X), + float32(min.Y), + float32(max.X), + float32(min.Y)} + + keyframe.texCoord = NewVBO(gl.ARRAY_BUFFER) + keyframe.texCoord.Fill(gl.Ptr(texData), 4, 8, gl.STATIC_DRAW) + + CheckGLError() + + return keyframe +} + +// A set of keyframes making up an animation. +type KeyframeSet struct { + Keyframes []Keyframe +} + +// Creates a new empty keyframe set with given size. +func NewKeyframeSet() *KeyframeSet { + set := &KeyframeSet{} + set.Keyframes = make([]Keyframe, 0) + + return set +} + +// Adds a new keyframe to set and returns new length. +func (s *KeyframeSet) Add(frame *Keyframe) int { + s.Keyframes = append(s.Keyframes, *frame) + return len(s.Keyframes) +} + +// Keyframe animation component. +// It has a start and an end frame, a play speed and option to loop. +type KeyframeAnimation struct { + Start, End int + Loop bool + Speed float64 + Current int + Interpolation float64 +} + +// Creates a new keyframe animation with given start, end and loop. +func NewKeyframeAnimation(start, end int, loop bool, speed float64) *KeyframeAnimation { + return &KeyframeAnimation{start, end, loop, speed, 0, 0} +} + +// An animated sprite is a sprite with keyframe animation information. +// It will be updated and rendered by the KeyframeRenderer. +type AnimatedSprite struct { + *Actor + *Pos2D + *Tex + *KeyframeSet + *KeyframeAnimation +} + +// Creates a new animated sprite. +func NewAnimatedSprite(tex *Tex, set *KeyframeSet, width, height int) *AnimatedSprite { + sprite := &AnimatedSprite{} + sprite.Actor = NewActor() + sprite.Pos2D = NewPos2D() + sprite.Tex = tex + sprite.KeyframeSet = set + sprite.Scale = Vec2{1, 1} + sprite.Visible = true + + if width > 0 && height > 0 { + sprite.Size = Vec2{float64(width), float64(height)} + } else { + sprite.Size = Vec2{tex.GetSize().X, tex.GetSize().Y} + } + + CheckGLError() + + return sprite +} + +// The keyframe renderer renders animated sprites. +// It has a 2D position component, to move all sprites at once. +type KeyframeRenderer struct { + Pos2D + + Shader *Shader + Camera *Camera + + sprites []AnimatedSprite + index, vertex *VBO + vao *VAO +} + +// Creates a new keyframe renderer using given shader and camera. +// If shader and/or camera are nil, the default one will be used. +func NewKeyframeRenderer(shader *Shader, camera *Camera) *KeyframeRenderer { + if shader == nil { + shader = Default2DShader + } + + if camera == nil { + camera = DefaultCamera + } + + var tc *VBO + + renderer := &KeyframeRenderer{} + renderer.Shader = shader + renderer.Camera = camera + renderer.sprites = make([]AnimatedSprite, 0) + renderer.index, renderer.vertex, tc = CreateRectMesh(false) + tc.Drop() // we don't need that VBO + renderer.Size = Vec2{1, 1} + renderer.Scale = Vec2{1, 1} + + renderer.vao = NewVAO() + renderer.vao.Bind() + renderer.Shader.EnableVertexAttribArrays() + renderer.index.Bind() + renderer.vertex.Bind() + renderer.vertex.AttribPointer(shader.GetAttribLocation(Default_shader_2D_vertex_attrib), 2, gl.FLOAT, false, 0) + renderer.vao.Unbind() + + CheckGLError() + + return renderer +} + +// Frees recources created by keyframe renderer. +// This is called automatically when system gets removed. +func (s *KeyframeRenderer) Cleanup() { + s.index.Drop() + s.vertex.Drop() + s.vao.Drop() +} + +// Adds animated sprite to the renderer. +func (s *KeyframeRenderer) Add(actor *Actor, pos *Pos2D, tex *Tex, set *KeyframeSet, animation *KeyframeAnimation) bool { + id := actor.GetId() + + for _, sprite := range s.sprites { + if id == sprite.Actor.GetId() { + return false + } + } + + s.sprites = append(s.sprites, AnimatedSprite{actor, pos, tex, set, animation}) + + return true +} + +// Removes animated sprite from renderer. +func (s *KeyframeRenderer) Remove(actor *Actor) bool { + return s.RemoveById(actor.GetId()) +} + +// Removes sprite from renderer by ID. +func (s *KeyframeRenderer) RemoveById(id ActorId) bool { + for i, sprite := range s.sprites { + if sprite.Actor.GetId() == id { + s.sprites = append(s.sprites[:i], s.sprites[i+1:]...) + return true + } + } + + return false +} + +// Removes all animated sprites. +func (s *KeyframeRenderer) RemoveAll() { + s.sprites = make([]AnimatedSprite, 0) +} + +// Returns number of sprites. +func (s *KeyframeRenderer) Len() int { + return len(s.sprites) +} + +func (s *KeyframeRenderer) GetName() string { + return keyframe_sprite_renderer_name +} + +// Updates animation state and renders sprites. +func (s *KeyframeRenderer) Update(delta float64) { + // update animation state + for i := range s.sprites { + if s.sprites[i].KeyframeAnimation == nil { + continue + } + + s.sprites[i].Interpolation += delta * s.sprites[i].KeyframeAnimation.Speed + + if s.sprites[i].Interpolation > 1 { + s.sprites[i].Interpolation = 0 + s.sprites[i].Current++ + + if s.sprites[i].Current > s.sprites[i].KeyframeAnimation.End { + if s.sprites[i].KeyframeAnimation.Loop { + s.sprites[i].Current = s.sprites[i].KeyframeAnimation.Start + } else { + s.sprites[i].Current = s.sprites[i].KeyframeAnimation.End + } + } + } + } + + // render + s.Shader.Bind() + s.Shader.SendMat3(Default_shader_2D_ortho, *MultMat3(s.Camera.CalcOrtho(), s.CalcModel())) + s.Shader.SendUniform1i(Default_shader_2D_tex, 0) + s.vao.Bind() + + for i := range s.sprites { + if !s.sprites[i].Visible { + continue + } + + texCoord := s.sprites[i].KeyframeSet.Keyframes[s.sprites[i].Current].texCoord + texCoord.Bind() + texCoord.AttribPointer(s.Shader.GetAttribLocation(Default_shader_2D_texcoord_attrib), 2, gl.FLOAT, false, 0) + + s.Shader.SendMat3(Default_shader_2D_model, *s.sprites[i].CalcModel()) + s.sprites[i].Tex.Bind() + + gl.DrawElements(gl.TRIANGLES, 6, gl.UNSIGNED_INT, nil) + } +}