Initial commit

This commit is contained in:
Trevor Slocum 2021-11-18 20:13:28 -08:00
commit 33ec0b1d86
24 changed files with 1005 additions and 0 deletions

21
LICENSE Normal file
View 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
View File

@ -0,0 +1,16 @@
# gohan
[![GoDoc](https://code.rocketnine.space/tslocum/godoc-static/raw/branch/master/badge.svg)](https://docs.rocketnine.space/code.rocketnine.space/tslocum/gohan)
[![Donate](https://img.shields.io/liberapay/receives/rocketnine.space.svg?logo=liberapay)](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
View 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
View File

@ -0,0 +1,4 @@
/*
Package gohan provides an Entity Component System framework for Ebiten.
*/
package gohan

13
entity.go Normal file
View 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
}

View File

@ -0,0 +1,8 @@
//go:build example
// +build example
package asset
import "github.com/hajimehoshi/ebiten/v2"
var ImgWhiteSquare = ebiten.NewImage(32, 32)

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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)
}

View 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)
}
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
View 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
View 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
View 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
View 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")
)