Initial commit
This commit is contained in:
commit
33ec0b1d86
24 changed files with 1005 additions and 0 deletions
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 Trevor Slocum <trevor@rocketnine.space>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
16
README.md
Normal file
16
README.md
Normal file
|
@ -0,0 +1,16 @@
|
|||
# gohan
|
||||
[](https://docs.rocketnine.space/code.rocketnine.space/tslocum/gohan)
|
||||
[](https://liberapay.com/rocketnine.space)
|
||||
|
||||
[Entity component system](https://en.wikipedia.org/wiki/Entity_component_system) framework for [Ebiten](https://ebiten.org)
|
||||
|
||||
**Note:** This framework is still in development. Breaking changes may be
|
||||
made until v1.0 is released.
|
||||
|
||||
## Documentation
|
||||
|
||||
Documentation is available via [godoc](https://docs.rocketnine.space/code.rocketnine.space/tslocum/gohan).
|
||||
|
||||
## Support
|
||||
|
||||
Please share issues and suggestions [here](https://code.rocketnine.space/tslocum/gohan/issues).
|
67
component.go
Normal file
67
component.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
package gohan
|
||||
|
||||
import "fmt"
|
||||
|
||||
// ComponentID is a component identifier. Each Component is assigned a unique ID
|
||||
// via NextComponentID, and implements a ComponentID method which returns the ID.
|
||||
type ComponentID int
|
||||
|
||||
// Component represents data for an entity, and how it interacts with the world.
|
||||
type Component interface {
|
||||
ComponentID() ComponentID
|
||||
}
|
||||
|
||||
var nextComponentID ComponentID
|
||||
|
||||
// NextComponentID returns the next available ComponentID.
|
||||
func NextComponentID() ComponentID {
|
||||
id := nextComponentID
|
||||
nextComponentID++
|
||||
return id
|
||||
}
|
||||
|
||||
func (entity EntityID) propagateChanges() {
|
||||
for i, system := range gameSystems {
|
||||
systemEntityIndex := -1
|
||||
for j, systemEntity := range gameSystemEntities[i] {
|
||||
if systemEntity == entity {
|
||||
systemEntityIndex = j
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if system.Matches(entity) {
|
||||
if systemEntityIndex != -1 {
|
||||
// Already attached.
|
||||
continue
|
||||
}
|
||||
|
||||
gameSystemEntities[i] = append(gameSystemEntities[i], entity)
|
||||
print(fmt.Sprintf("Attached entity %d to system %d.", entity, i))
|
||||
} else if systemEntityIndex != -1 {
|
||||
// Detach from system.
|
||||
gameSystemEntities[i] = append(gameSystemEntities[i][:systemEntityIndex], gameSystemEntities[i][systemEntityIndex+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AddComponent adds a Component to an Entity.
|
||||
func (entity EntityID) AddComponent(component Component) {
|
||||
componentID := component.ComponentID()
|
||||
|
||||
if gameComponents[entity] == nil {
|
||||
gameComponents[entity] = make(map[ComponentID]interface{})
|
||||
}
|
||||
gameComponents[entity][componentID] = component
|
||||
|
||||
entity.propagateChanges()
|
||||
}
|
||||
|
||||
// Component gets a Component of an Entity.
|
||||
func (entity EntityID) Component(componentID ComponentID) interface{} {
|
||||
components := gameComponents[entity]
|
||||
if components == nil {
|
||||
return nil
|
||||
}
|
||||
return components[componentID]
|
||||
}
|
4
doc.go
Normal file
4
doc.go
Normal file
|
@ -0,0 +1,4 @@
|
|||
/*
|
||||
Package gohan provides an Entity Component System framework for Ebiten.
|
||||
*/
|
||||
package gohan
|
13
entity.go
Normal file
13
entity.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package gohan
|
||||
|
||||
// EntityID is an entity identifier.
|
||||
type EntityID int
|
||||
|
||||
var nextEntityID EntityID
|
||||
|
||||
// NextEntityID returns the next available EntityID.
|
||||
func NextEntityID() EntityID {
|
||||
entityID := nextEntityID
|
||||
nextEntityID++
|
||||
return entityID
|
||||
}
|
8
examples/twinstick/asset/asset.go
Normal file
8
examples/twinstick/asset/asset.go
Normal file
|
@ -0,0 +1,8 @@
|
|||
//go:build example
|
||||
// +build example
|
||||
|
||||
package asset
|
||||
|
||||
import "github.com/hajimehoshi/ebiten/v2"
|
||||
|
||||
var ImgWhiteSquare = ebiten.NewImage(32, 32)
|
17
examples/twinstick/component/bullet.go
Normal file
17
examples/twinstick/component/bullet.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
//go:build example
|
||||
// +build example
|
||||
|
||||
package component
|
||||
|
||||
import (
|
||||
"code.rocketnine.space/tslocum/gohan"
|
||||
)
|
||||
|
||||
type BulletComponent struct {
|
||||
}
|
||||
|
||||
var BulletComponentID = gohan.NextComponentID()
|
||||
|
||||
func (p *BulletComponent) ComponentID() gohan.ComponentID {
|
||||
return BulletComponentID
|
||||
}
|
18
examples/twinstick/component/position.go
Normal file
18
examples/twinstick/component/position.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
//go:build example
|
||||
// +build example
|
||||
|
||||
package component
|
||||
|
||||
import (
|
||||
"code.rocketnine.space/tslocum/gohan"
|
||||
)
|
||||
|
||||
type PositionComponent struct {
|
||||
X, Y float64
|
||||
}
|
||||
|
||||
var PositionComponentID = gohan.NextComponentID()
|
||||
|
||||
func (p *PositionComponent) ComponentID() gohan.ComponentID {
|
||||
return PositionComponentID
|
||||
}
|
18
examples/twinstick/component/velocity.go
Normal file
18
examples/twinstick/component/velocity.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
//go:build example
|
||||
// +build example
|
||||
|
||||
package component
|
||||
|
||||
import (
|
||||
"code.rocketnine.space/tslocum/gohan"
|
||||
)
|
||||
|
||||
type VelocityComponent struct {
|
||||
X, Y float64
|
||||
}
|
||||
|
||||
var VelocityComponentID = gohan.NextComponentID()
|
||||
|
||||
func (c *VelocityComponent) ComponentID() gohan.ComponentID {
|
||||
return VelocityComponentID
|
||||
}
|
27
examples/twinstick/component/weapon.go
Normal file
27
examples/twinstick/component/weapon.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
//go:build example
|
||||
// +build example
|
||||
|
||||
package component
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"code.rocketnine.space/tslocum/gohan"
|
||||
)
|
||||
|
||||
type WeaponComponent struct {
|
||||
Ammo int
|
||||
|
||||
Damage int
|
||||
|
||||
FireRate time.Duration
|
||||
LastFire time.Time
|
||||
|
||||
BulletSpeed float64
|
||||
}
|
||||
|
||||
var WeaponComponentID = gohan.NextComponentID()
|
||||
|
||||
func (p *WeaponComponent) ComponentID() gohan.ComponentID {
|
||||
return WeaponComponentID
|
||||
}
|
27
examples/twinstick/entity/bullet.go
Normal file
27
examples/twinstick/entity/bullet.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
//go:build example
|
||||
// +build example
|
||||
|
||||
package entity
|
||||
|
||||
import (
|
||||
"code.rocketnine.space/tslocum/gohan"
|
||||
"code.rocketnine.space/tslocum/gohan/_examples/twinstick/component"
|
||||
)
|
||||
|
||||
func NewBullet(x, y, xSpeed, ySpeed float64) gohan.EntityID {
|
||||
bullet := gohan.NextEntityID()
|
||||
|
||||
bullet.AddComponent(&component.PositionComponent{
|
||||
X: x,
|
||||
Y: y,
|
||||
})
|
||||
|
||||
bullet.AddComponent(&component.VelocityComponent{
|
||||
X: xSpeed,
|
||||
Y: ySpeed,
|
||||
})
|
||||
|
||||
bullet.AddComponent(&component.BulletComponent{})
|
||||
|
||||
return bullet
|
||||
}
|
36
examples/twinstick/entity/player.go
Normal file
36
examples/twinstick/entity/player.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
//go:build example
|
||||
// +build example
|
||||
|
||||
package entity
|
||||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"code.rocketnine.space/tslocum/gohan"
|
||||
"code.rocketnine.space/tslocum/gohan/_examples/twinstick/component"
|
||||
)
|
||||
|
||||
func NewPlayer() gohan.EntityID {
|
||||
player := gohan.NextEntityID()
|
||||
|
||||
// Set position to -1,-1 to indicate the player has not been assigned a
|
||||
// position yet. We will place the player in the center of the screen when
|
||||
// we receive the screen dimensions for the first time.
|
||||
player.AddComponent(&component.PositionComponent{
|
||||
X: -1,
|
||||
Y: -1,
|
||||
})
|
||||
|
||||
player.AddComponent(&component.VelocityComponent{})
|
||||
|
||||
weapon := &component.WeaponComponent{
|
||||
Ammo: math.MaxInt64,
|
||||
Damage: 1,
|
||||
FireRate: 100 * time.Millisecond,
|
||||
BulletSpeed: 15,
|
||||
}
|
||||
player.AddComponent(weapon)
|
||||
|
||||
return player
|
||||
}
|
113
examples/twinstick/game/game.go
Normal file
113
examples/twinstick/game/game.go
Normal file
|
@ -0,0 +1,113 @@
|
|||
//go:build example
|
||||
// +build example
|
||||
|
||||
package game
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"code.rocketnine.space/tslocum/gohan"
|
||||
"code.rocketnine.space/tslocum/gohan/_examples/twinstick/asset"
|
||||
"code.rocketnine.space/tslocum/gohan/_examples/twinstick/component"
|
||||
"code.rocketnine.space/tslocum/gohan/_examples/twinstick/entity"
|
||||
"code.rocketnine.space/tslocum/gohan/_examples/twinstick/system"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
||||
// game is an isometric demo game.
|
||||
type game struct {
|
||||
w, h int
|
||||
|
||||
player gohan.EntityID
|
||||
|
||||
op *ebiten.DrawImageOptions
|
||||
|
||||
disableEsc bool
|
||||
|
||||
debugMode bool
|
||||
cpuProfile *os.File
|
||||
|
||||
movementSystem *system.MovementSystem
|
||||
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
// NewGame returns a new isometric demo game.
|
||||
func NewGame() (*game, error) {
|
||||
g := &game{
|
||||
op: &ebiten.DrawImageOptions{},
|
||||
}
|
||||
|
||||
g.addSystems()
|
||||
|
||||
err := g.loadAssets()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
g.player = entity.NewPlayer()
|
||||
|
||||
asset.ImgWhiteSquare.Fill(color.White)
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
// Layout is called when the game's layout changes.
|
||||
func (g *game) Layout(outsideWidth, outsideHeight int) (int, int) {
|
||||
s := ebiten.DeviceScaleFactor()
|
||||
w, h := int(s*float64(outsideWidth)), int(s*float64(outsideHeight))
|
||||
if w != g.w || h != g.h {
|
||||
g.w, g.h = w, h
|
||||
g.movementSystem.ScreenW, g.movementSystem.ScreenH = float64(w), float64(h)
|
||||
|
||||
position := g.player.Component(component.PositionComponentID).(*component.PositionComponent)
|
||||
if position.X == -1 && position.Y == -1 {
|
||||
position.X, position.Y = float64(g.w)/2-16, float64(g.h)/2-16
|
||||
}
|
||||
}
|
||||
return g.w, g.h
|
||||
}
|
||||
|
||||
func (g *game) Update() error {
|
||||
if ebiten.IsWindowBeingClosed() {
|
||||
g.Exit()
|
||||
return nil
|
||||
}
|
||||
|
||||
return gohan.Update()
|
||||
}
|
||||
|
||||
func (g *game) Draw(screen *ebiten.Image) {
|
||||
err := gohan.Draw(screen)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *game) addSystems() {
|
||||
gohan.AddSystem(system.NewMovementInputSystem(g.player))
|
||||
|
||||
g.movementSystem = &system.MovementSystem{}
|
||||
gohan.AddSystem(g.movementSystem)
|
||||
|
||||
gohan.AddSystem(system.NewFireInputSystem(g.player))
|
||||
|
||||
renderBullet := system.NewDrawBulletsSystem()
|
||||
gohan.AddSystem(renderBullet)
|
||||
|
||||
renderPlayer := system.NewDrawPlayerSystem(g.player)
|
||||
gohan.AddSystem(renderPlayer)
|
||||
|
||||
printInfo := system.NewPrintInfoSystem(g.player)
|
||||
gohan.AddSystem(printInfo)
|
||||
}
|
||||
|
||||
func (g *game) loadAssets() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *game) Exit() {
|
||||
os.Exit(0)
|
||||
}
|
45
examples/twinstick/main.go
Normal file
45
examples/twinstick/main.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
//go:build example
|
||||
// +build example
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"code.rocketnine.space/tslocum/gohan/_examples/twinstick/game"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ebiten.SetWindowTitle("Twin-Stick Shooter Example - Gohan")
|
||||
ebiten.SetWindowResizable(true)
|
||||
ebiten.SetFullscreen(true)
|
||||
ebiten.SetMaxTPS(144)
|
||||
ebiten.SetRunnableOnUnfocused(true)
|
||||
ebiten.SetWindowClosingHandled(true)
|
||||
ebiten.SetFPSMode(ebiten.FPSModeVsyncOn)
|
||||
ebiten.SetCursorShape(ebiten.CursorShapeCrosshair)
|
||||
|
||||
g, err := game.NewGame()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
sigc := make(chan os.Signal, 1)
|
||||
signal.Notify(sigc,
|
||||
syscall.SIGINT,
|
||||
syscall.SIGTERM)
|
||||
go func() {
|
||||
<-sigc
|
||||
|
||||
g.Exit()
|
||||
}()
|
||||
|
||||
err = ebiten.RunGame(g)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
43
examples/twinstick/system/draw_bullets.go
Normal file
43
examples/twinstick/system/draw_bullets.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
//go:build example
|
||||
// +build example
|
||||
|
||||
package system
|
||||
|
||||
import (
|
||||
"code.rocketnine.space/tslocum/gohan"
|
||||
"code.rocketnine.space/tslocum/gohan/_examples/twinstick/asset"
|
||||
"code.rocketnine.space/tslocum/gohan/_examples/twinstick/component"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
||||
type DrawBulletsSystem struct {
|
||||
op *ebiten.DrawImageOptions
|
||||
}
|
||||
|
||||
func NewDrawBulletsSystem() *DrawBulletsSystem {
|
||||
return &DrawBulletsSystem{
|
||||
op: &ebiten.DrawImageOptions{},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DrawBulletsSystem) Matches(entity gohan.EntityID) bool {
|
||||
position := entity.Component(component.PositionComponentID)
|
||||
bullet := entity.Component(component.BulletComponentID)
|
||||
|
||||
return position != nil && bullet != nil
|
||||
}
|
||||
|
||||
func (s *DrawBulletsSystem) Update(_ gohan.EntityID) error {
|
||||
return gohan.ErrSystemWithoutUpdate
|
||||
}
|
||||
|
||||
func (s *DrawBulletsSystem) Draw(entity gohan.EntityID, screen *ebiten.Image) error {
|
||||
position := entity.Component(component.PositionComponentID).(*component.PositionComponent)
|
||||
|
||||
s.op.GeoM.Reset()
|
||||
s.op.GeoM.Translate(-16, -16)
|
||||
s.op.GeoM.Scale(0.5, 0.5)
|
||||
s.op.GeoM.Translate(position.X, position.Y)
|
||||
screen.DrawImage(asset.ImgWhiteSquare, s.op)
|
||||
return nil
|
||||
}
|
40
examples/twinstick/system/draw_player.go
Normal file
40
examples/twinstick/system/draw_player.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
//go:build example
|
||||
// +build example
|
||||
|
||||
package system
|
||||
|
||||
import (
|
||||
"code.rocketnine.space/tslocum/gohan"
|
||||
"code.rocketnine.space/tslocum/gohan/_examples/twinstick/asset"
|
||||
"code.rocketnine.space/tslocum/gohan/_examples/twinstick/component"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
||||
type drawPlayerSystem struct {
|
||||
player gohan.EntityID
|
||||
op *ebiten.DrawImageOptions
|
||||
}
|
||||
|
||||
func NewDrawPlayerSystem(player gohan.EntityID) *drawPlayerSystem {
|
||||
return &drawPlayerSystem{
|
||||
player: player,
|
||||
op: &ebiten.DrawImageOptions{},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *drawPlayerSystem) Matches(entity gohan.EntityID) bool {
|
||||
return entity == s.player
|
||||
}
|
||||
|
||||
func (s *drawPlayerSystem) Update(_ gohan.EntityID) error {
|
||||
return gohan.ErrSystemWithoutUpdate
|
||||
}
|
||||
|
||||
func (s *drawPlayerSystem) Draw(entity gohan.EntityID, screen *ebiten.Image) error {
|
||||
position := entity.Component(component.PositionComponentID).(*component.PositionComponent)
|
||||
|
||||
s.op.GeoM.Reset()
|
||||
s.op.GeoM.Translate(position.X-16, position.Y-16)
|
||||
screen.DrawImage(asset.ImgWhiteSquare, s.op)
|
||||
return nil
|
||||
}
|
90
examples/twinstick/system/input_fire.go
Normal file
90
examples/twinstick/system/input_fire.go
Normal file
|
@ -0,0 +1,90 @@
|
|||
//go:build example
|
||||
// +build example
|
||||
|
||||
package system
|
||||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"code.rocketnine.space/tslocum/gohan"
|
||||
"code.rocketnine.space/tslocum/gohan/_examples/twinstick/component"
|
||||
"code.rocketnine.space/tslocum/gohan/_examples/twinstick/entity"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
||||
func angle(x1, y1, x2, y2 float64) float64 {
|
||||
return math.Atan2(y1-y2, x1-x2)
|
||||
}
|
||||
|
||||
type fireInputSystem struct {
|
||||
player gohan.EntityID
|
||||
}
|
||||
|
||||
func NewFireInputSystem(player gohan.EntityID) *fireInputSystem {
|
||||
return &fireInputSystem{
|
||||
player: player,
|
||||
}
|
||||
}
|
||||
|
||||
func (_ *fireInputSystem) Matches(e gohan.EntityID) bool {
|
||||
weapon := e.Component(component.WeaponComponentID)
|
||||
|
||||
return weapon != nil
|
||||
}
|
||||
|
||||
func (s *fireInputSystem) fire(weapon *component.WeaponComponent, position *component.PositionComponent, fireAngle float64) {
|
||||
if time.Since(weapon.LastFire) < weapon.FireRate {
|
||||
return
|
||||
}
|
||||
|
||||
weapon.Ammo--
|
||||
weapon.LastFire = time.Now()
|
||||
|
||||
speedX := math.Cos(fireAngle) * -weapon.BulletSpeed
|
||||
speedY := math.Sin(fireAngle) * -weapon.BulletSpeed
|
||||
|
||||
bullet := entity.NewBullet(position.X, position.Y, speedX, speedY)
|
||||
_ = bullet
|
||||
}
|
||||
|
||||
func (s *fireInputSystem) Update(_ gohan.EntityID) error {
|
||||
weapon := s.player.Component(component.WeaponComponentID).(*component.WeaponComponent)
|
||||
|
||||
if weapon.Ammo <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
position := s.player.Component(component.PositionComponentID).(*component.PositionComponent)
|
||||
|
||||
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
|
||||
cursorX, cursorY := ebiten.CursorPosition()
|
||||
fireAngle := angle(position.X, position.Y, float64(cursorX), float64(cursorY))
|
||||
s.fire(weapon, position, fireAngle)
|
||||
}
|
||||
|
||||
switch {
|
||||
case ebiten.IsKeyPressed(ebiten.KeyLeft) && ebiten.IsKeyPressed(ebiten.KeyUp):
|
||||
s.fire(weapon, position, math.Pi/4)
|
||||
case ebiten.IsKeyPressed(ebiten.KeyLeft) && ebiten.IsKeyPressed(ebiten.KeyDown):
|
||||
s.fire(weapon, position, -math.Pi/4)
|
||||
case ebiten.IsKeyPressed(ebiten.KeyRight) && ebiten.IsKeyPressed(ebiten.KeyUp):
|
||||
s.fire(weapon, position, math.Pi*.75)
|
||||
case ebiten.IsKeyPressed(ebiten.KeyRight) && ebiten.IsKeyPressed(ebiten.KeyDown):
|
||||
s.fire(weapon, position, -math.Pi*.75)
|
||||
case ebiten.IsKeyPressed(ebiten.KeyLeft):
|
||||
s.fire(weapon, position, 0)
|
||||
case ebiten.IsKeyPressed(ebiten.KeyRight):
|
||||
s.fire(weapon, position, math.Pi)
|
||||
case ebiten.IsKeyPressed(ebiten.KeyUp):
|
||||
s.fire(weapon, position, math.Pi/2)
|
||||
case ebiten.IsKeyPressed(ebiten.KeyDown):
|
||||
s.fire(weapon, position, -math.Pi/2)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (_ *fireInputSystem) Draw(_ gohan.EntityID, _ *ebiten.Image) error {
|
||||
return gohan.ErrSystemWithoutDraw
|
||||
}
|
57
examples/twinstick/system/input_move.go
Normal file
57
examples/twinstick/system/input_move.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
//go:build example
|
||||
// +build example
|
||||
|
||||
package system
|
||||
|
||||
import (
|
||||
"code.rocketnine.space/tslocum/gohan"
|
||||
"code.rocketnine.space/tslocum/gohan/_examples/twinstick/component"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
||||
type movementInputSystem struct {
|
||||
player gohan.EntityID
|
||||
}
|
||||
|
||||
func NewMovementInputSystem(player gohan.EntityID) *movementInputSystem {
|
||||
return &movementInputSystem{
|
||||
player: player,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *movementInputSystem) Matches(e gohan.EntityID) bool {
|
||||
return e == s.player
|
||||
}
|
||||
|
||||
func (s *movementInputSystem) Update(e gohan.EntityID) error {
|
||||
velocity := s.player.Component(component.VelocityComponentID).(*component.VelocityComponent)
|
||||
if ebiten.IsKeyPressed(ebiten.KeyA) {
|
||||
velocity.X -= 0.5
|
||||
if velocity.X < -5 {
|
||||
velocity.X = -5
|
||||
}
|
||||
}
|
||||
if ebiten.IsKeyPressed(ebiten.KeyD) {
|
||||
velocity.X += 0.5
|
||||
if velocity.X > 5 {
|
||||
velocity.X = 5
|
||||
}
|
||||
}
|
||||
if ebiten.IsKeyPressed(ebiten.KeyW) {
|
||||
velocity.Y -= 0.5
|
||||
if velocity.Y < -5 {
|
||||
velocity.Y = -5
|
||||
}
|
||||
}
|
||||
if ebiten.IsKeyPressed(ebiten.KeyS) {
|
||||
velocity.Y += 0.5
|
||||
if velocity.Y > 5 {
|
||||
velocity.Y = 5
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *movementInputSystem) Draw(_ gohan.EntityID, _ *ebiten.Image) error {
|
||||
return gohan.ErrSystemWithoutDraw
|
||||
}
|
59
examples/twinstick/system/movement.go
Normal file
59
examples/twinstick/system/movement.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
//go:build example
|
||||
// +build example
|
||||
|
||||
package system
|
||||
|
||||
import (
|
||||
"code.rocketnine.space/tslocum/gohan"
|
||||
"code.rocketnine.space/tslocum/gohan/_examples/twinstick/component"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
||||
type MovementSystem struct {
|
||||
ScreenW, ScreenH float64
|
||||
}
|
||||
|
||||
func (_ *MovementSystem) Matches(entity gohan.EntityID) bool {
|
||||
position := entity.Component(component.PositionComponentID)
|
||||
velocity := entity.Component(component.VelocityComponentID)
|
||||
|
||||
return position != nil && velocity != nil
|
||||
}
|
||||
|
||||
func (s *MovementSystem) Update(entity gohan.EntityID) error {
|
||||
position := entity.Component(component.PositionComponentID).(*component.PositionComponent)
|
||||
velocity := entity.Component(component.VelocityComponentID).(*component.VelocityComponent)
|
||||
|
||||
bullet := entity.Component(component.BulletComponentID)
|
||||
|
||||
// Check for collision.
|
||||
if bullet == nil {
|
||||
if position.X+velocity.X < 16 {
|
||||
position.X = 16
|
||||
velocity.X = 0
|
||||
} else if position.X+velocity.X > s.ScreenW-16 {
|
||||
position.X = s.ScreenW - 16
|
||||
velocity.X = 0
|
||||
}
|
||||
if position.Y+velocity.Y < 16 {
|
||||
position.Y = 16
|
||||
velocity.Y = 0
|
||||
} else if position.Y+velocity.Y > s.ScreenH-16 {
|
||||
position.Y = s.ScreenH - 16
|
||||
velocity.Y = 0
|
||||
}
|
||||
}
|
||||
|
||||
position.X, position.Y = position.X+velocity.X, position.Y+velocity.Y
|
||||
|
||||
if bullet == nil {
|
||||
velocity.X *= 0.95
|
||||
velocity.Y *= 0.95
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (_ *MovementSystem) Draw(entity gohan.EntityID, screen *ebiten.Image) error {
|
||||
return gohan.ErrSystemWithoutDraw
|
||||
}
|
43
examples/twinstick/system/printinfo.go
Normal file
43
examples/twinstick/system/printinfo.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
//go:build example
|
||||
// +build example
|
||||
|
||||
package system
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.rocketnine.space/tslocum/gohan"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
||||
)
|
||||
|
||||
type printInfoSystem struct {
|
||||
img *ebiten.Image
|
||||
op *ebiten.DrawImageOptions
|
||||
player gohan.EntityID
|
||||
}
|
||||
|
||||
func NewPrintInfoSystem(player gohan.EntityID) *printInfoSystem {
|
||||
p := &printInfoSystem{
|
||||
img: ebiten.NewImage(200, 100),
|
||||
op: &ebiten.DrawImageOptions{},
|
||||
player: player,
|
||||
}
|
||||
p.op.GeoM.Scale(2, 2)
|
||||
return p
|
||||
}
|
||||
|
||||
func (s *printInfoSystem) Matches(e gohan.EntityID) bool {
|
||||
return e == s.player
|
||||
}
|
||||
|
||||
func (s *printInfoSystem) Update(_ gohan.EntityID) error {
|
||||
return gohan.ErrSystemWithoutUpdate
|
||||
}
|
||||
|
||||
func (s *printInfoSystem) Draw(entity gohan.EntityID, screen *ebiten.Image) error {
|
||||
s.img.Clear()
|
||||
ebitenutil.DebugPrint(s.img, fmt.Sprintf("KEY WASD+MOUSE\nTPS %0.0f\nFPS %0.0f", ebiten.CurrentTPS(), ebiten.CurrentFPS()))
|
||||
screen.DrawImage(s.img, s.op)
|
||||
return nil
|
||||
}
|
15
go.mod
Normal file
15
go.mod
Normal file
|
@ -0,0 +1,15 @@
|
|||
module code.rocketnine.space/tslocum/gohan
|
||||
|
||||
go 1.17
|
||||
|
||||
require github.com/hajimehoshi/ebiten/v2 v2.2.2
|
||||
|
||||
require (
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be // indirect
|
||||
github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240 // indirect
|
||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect
|
||||
golang.org/x/mobile v0.0.0-20210902104108-5d9a33257ab5 // indirect
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
||||
golang.org/x/sys v0.0.0-20210917161153-d61c044b1678 // indirect
|
||||
)
|
66
go.sum
Normal file
66
go.sum
Normal file
|
@ -0,0 +1,66 @@
|
|||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be h1:vEIVIuBApEBQTEJt19GfhoU+zFSV+sNTa9E9FdnRYfk=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/hajimehoshi/bitmapfont/v2 v2.1.3/go.mod h1:2BnYrkTQGThpr/CY6LorYtt/zEPNzvE/ND69CRTaHMs=
|
||||
github.com/hajimehoshi/ebiten/v2 v2.2.2 h1:92E+ogdNyH1P/LlvMQ7vonbFDh6bl+O7Ak+H1HX0RX8=
|
||||
github.com/hajimehoshi/ebiten/v2 v2.2.2/go.mod h1:olKl/qqhMBBAm2oI7Zy292nCtE+nitlmYKNF3UpbFn0=
|
||||
github.com/hajimehoshi/file2byteslice v0.0.0-20210813153925-5340248a8f41/go.mod h1:CqqAHp7Dk/AqQiwuhV1yT2334qbA/tFWQW0MD2dGqUE=
|
||||
github.com/hajimehoshi/go-mp3 v0.3.2/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
|
||||
github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
|
||||
github.com/hajimehoshi/oto/v2 v2.1.0-alpha.2/go.mod h1:rUKQmwMkqmRxe+IAof9+tuYA2ofm8cAWXFmSfzDN8vQ=
|
||||
github.com/jakecoffman/cp v1.1.0/go.mod h1:JjY/Fp6d8E1CHnu74gWNnU0+b9VzEdUVPoJxg2PsTQg=
|
||||
github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240 h1:dy+DS31tGEGCsZzB45HmJJNHjur8GDgtRNX9U7HnSX4=
|
||||
github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240/go.mod h1:3P4UH/k22rXyHIJD2w4h2XMqPX4Of/eySEZq9L6wqc4=
|
||||
github.com/jfreymuth/oggvorbis v1.0.3/go.mod h1:1U4pqWmghcoVsCJJ4fRBKv9peUJMBHixthRlBeD6uII=
|
||||
github.com/jfreymuth/vorbis v1.0.2/go.mod h1:DoftRo4AznKnShRl1GxiTFCseHr4zR9BN3TWXyuzrqQ=
|
||||
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU=
|
||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs=
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mobile v0.0.0-20210902104108-5d9a33257ab5 h1:peBP2oZO/xVnGMaWMCyFEI0WENsGj71wx5K12mRELHQ=
|
||||
golang.org/x/mobile v0.0.0-20210902104108-5d9a33257ab5/go.mod h1:c4YKU3ZylDmvbw+H/PSvm42vhdWbuxCzbonauEAP9B8=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210917161153-d61c044b1678 h1:J27LZFQBFoihqXoegpscI10HpjZ7B5WQLLKL2FZXQKw=
|
||||
golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.6/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
129
gohan.go
Normal file
129
gohan.go
Normal file
|
@ -0,0 +1,129 @@
|
|||
package gohan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
gameComponents = make(map[EntityID]map[ComponentID]interface{})
|
||||
|
||||
gameSystems []System
|
||||
gameSystemEntities [][]EntityID // Slice of entities matching each system.
|
||||
|
||||
gameSystemReceivesUpdate []bool
|
||||
gameSystemReceivesDraw []bool
|
||||
|
||||
debug bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
debugEnv := os.Getenv("GOHAN_DEBUG")
|
||||
debugEnv = strings.TrimSpace(debugEnv)
|
||||
debugEnv = strings.ToLower(debugEnv)
|
||||
|
||||
debug = debugEnv == "1" || debugEnv == "t" || debugEnv == "y" || debugEnv == "on" || debugEnv == "yes" || debugEnv == "true"
|
||||
}
|
||||
|
||||
// print prints debug information (when enabled).
|
||||
func print(s string) {
|
||||
if !debug {
|
||||
return
|
||||
}
|
||||
|
||||
log.Println(s)
|
||||
}
|
||||
|
||||
func attachEntitiesToSystem(system System) {
|
||||
// This function is always called on a newly added system.
|
||||
systemID := len(gameSystemEntities) - 1
|
||||
|
||||
for entity := EntityID(0); entity < nextEntityID; entity++ {
|
||||
if system.Matches(entity) {
|
||||
gameSystemEntities[systemID] = append(gameSystemEntities[systemID], entity)
|
||||
|
||||
print(fmt.Sprintf("Attached entity %d to system %d.", entity, systemID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterSystem registers a system to start receiving Update and Draw calls.
|
||||
func RegisterSystem(system System) {
|
||||
gameSystems = append(gameSystems, system)
|
||||
gameSystemReceivesUpdate = append(gameSystemReceivesUpdate, true)
|
||||
gameSystemReceivesDraw = append(gameSystemReceivesDraw, true)
|
||||
gameSystemEntities = append(gameSystemEntities, nil)
|
||||
|
||||
attachEntitiesToSystem(system)
|
||||
}
|
||||
|
||||
func updateSystem(i int) (int, error) {
|
||||
updated := 0
|
||||
for _, entity := range gameSystemEntities[i] {
|
||||
err := gameSystems[i].Update(entity)
|
||||
if err != nil {
|
||||
if err == ErrSystemWithoutUpdate {
|
||||
// Unregister system from Update events.
|
||||
gameSystemReceivesUpdate[i] = false
|
||||
return 0, nil
|
||||
}
|
||||
return 0, fmt.Errorf("failed to update system %d for entity %d: %s", i, entity, err)
|
||||
}
|
||||
updated++
|
||||
}
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
// Update updates the game state.
|
||||
func Update() error {
|
||||
var t time.Time
|
||||
if debug {
|
||||
t = time.Now()
|
||||
}
|
||||
var systems int
|
||||
for i, registered := range gameSystemReceivesUpdate {
|
||||
if !registered {
|
||||
continue
|
||||
}
|
||||
|
||||
updated, err := updateSystem(i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
print(fmt.Sprintf("System %d: updated %d entities.", i, updated))
|
||||
systems++
|
||||
}
|
||||
if debug {
|
||||
print(fmt.Sprintf("Finished updating %d systems in %.2fms.", systems, float64(time.Since(t).Microseconds())/1000))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Draw draws the game on to the screen.
|
||||
func Draw(screen *ebiten.Image) error {
|
||||
DRAWSYSTEMS:
|
||||
for i, registered := range gameSystemReceivesDraw {
|
||||
if !registered {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, entity := range gameSystemEntities[i] {
|
||||
err := gameSystems[i].Draw(entity, screen)
|
||||
if err != nil {
|
||||
if err == ErrSystemWithoutDraw {
|
||||
// Unregister system from Draw events.
|
||||
gameSystemReceivesDraw[i] = false
|
||||
continue DRAWSYSTEMS
|
||||
}
|
||||
return fmt.Errorf("failed to draw system %d for entity %d: %s", i, entity, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
33
system.go
Normal file
33
system.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
package gohan
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
||||
// System represents a system that runs continuously. While the system must
|
||||
// implement the Update and Draw methods, a special error value may be returned
|
||||
// indicating that the system does not utilize one of the methods. System
|
||||
// methods which return one of these special error values will not be called again.
|
||||
//
|
||||
// See ErrSystemWithoutUpdate and ErrSystemWithoutDraw.
|
||||
type System interface {
|
||||
// Matches returns whether the provided entity is handled by this system.
|
||||
Matches(entity EntityID) bool
|
||||
|
||||
// Update is called once for each matching entity each time the game state is updated.
|
||||
Update(entity EntityID) error
|
||||
|
||||
// Draw is called once for each matching entity each time the game is drawn to the screen.
|
||||
Draw(entity EntityID, screen *ebiten.Image) error
|
||||
}
|
||||
|
||||
// Special error values.
|
||||
var (
|
||||
// ErrSystemWithoutUpdate is the error returned when a System does not implement Update.
|
||||
ErrSystemWithoutUpdate = errors.New("system does not implement update")
|
||||
|
||||
// ErrSystemWithoutDraw is the error returned when a System does not implement Draw.
|
||||
ErrSystemWithoutDraw = errors.New("system does not implement draw")
|
||||
)
|
Loading…
Reference in a new issue