commit
33ec0b1d86
24 changed files with 1005 additions and 0 deletions
@ -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. |
@ -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). |
@ -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] |
||||
} |
@ -0,0 +1,4 @@
|
||||
/* |
||||
Package gohan provides an Entity Component System framework for Ebiten. |
||||
*/ |
||||
package gohan |
@ -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 |
||||
} |
@ -0,0 +1,8 @@
|
||||
//go:build example
|
||||
// +build example
|
||||
|
||||
package asset |
||||
|
||||
import "github.com/hajimehoshi/ebiten/v2" |
||||
|
||||
var ImgWhiteSquare = ebiten.NewImage(32, 32) |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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) |
||||
} |
@ -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) |
||||
} |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
) |
@ -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= |
@ -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 |
||||
} |
@ -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 new issue