A Go implementation of BulletML
You can play a BulletML web simulator from here.
https://pkg.go.dev/github.com/tsujio/go-bulletml
The BulletML specifications are here
Important
go-bulletml and many BulletML libraries (and also the original) run top-level
<action>
elements that havelabel
attribute and the value starts with"top"
as entry points. For example:
<action label="top">
<action label="top-1">
<?xml version="1.0" ?>
<!DOCTYPE bulletml SYSTEM "http://www.asahi-net.or.jp/~cs8k-cyu/bulletml/bulletml.dtd">
<bulletml type="vertical" xmlns="http://www.asahi-net.or.jp/~cs8k-cyu/bulletml">
<action label="top">
<repeat>
<times>999</times>
<action>
<fire>
<direction type="sequence">-5</direction>
<bullet />
</fire>
<repeat>
<times>7</times>
<action>
<fire>
<direction type="sequence">45</direction>
<bullet />
</fire>
</action>
</repeat>
<wait>2</wait>
</action>
</repeat>
</action>
</bulletml>
f, err := os.Open("bulletml.xml")
if err != nil {
panic(err)
}
defer f.Close()
bml, err := bulletml.Load(f)
if err != nil {
panic(err)
}
player := &Player{}
enemy := &Enemy{}
bullets := make([]*Bullet)
opts := &bulletml.NewRunnerOptions{
// Called when new bullet fired
OnBulletFired: func(bulletRunner bulletml.BulletRunner, _ *bulletml.FireContext) {
b := &Bullet{
runner: bulletRunner,
}
b.x, b.y = bulletRunner.Position()
bullets = append(bullets, b)
},
// Tell current enemy position
CurrentShootPosition: func() (float64, float64) {
return enemy.x, enemy.y
},
// Tell current player position
CurrentTargetPosition: func() (float64, float64) {
return player.x, player.y
},
}
runner, err := bulletml.NewRunner(bml, opts)
if err != nil {
panic(err)
}
enemy.runner = runner
if err := enemy.runner.Update(); err != nil {
panic(err)
}
// bullets may be extended in b.runner.Update (by calling bulletml.NewRunnerOptions.OnBulletFired), so you should write loop like this.
for i, n := 0, len(bullets); i < n; i++ {
b := bullets[i]
if err := b.runner.Update(); err != nil {
panic(err)
}
b.x, b.y = b.runner.Position()
}
This sample uses Ebitengine, which is a simple Go game engine.
package main
import (
"image/color"
"os"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/vector"
"github.com/tsujio/go-bulletml"
)
const (
screenWidth = 640
screenHeight = 480
)
type Player struct {
x, y float64
}
type Enemy struct {
x, y float64
runner bulletml.Runner
}
type Bullet struct {
x, y float64
runner bulletml.BulletRunner
}
type Game struct {
player *Player
enemies []*Enemy
bullets []*Bullet
}
func (g *Game) addEnemy(x, y float64) {
// Open your BulletML file
f, err := os.Open("bulletml.xml")
if err != nil {
panic(err)
}
defer f.Close()
// Load data
bml, err := bulletml.Load(f)
if err != nil {
panic(err)
}
enemy := &Enemy{x: x, y: y}
// Create BulletML runner option
opts := &bulletml.NewRunnerOptions{
// Called when new bullet fired
OnBulletFired: func(bulletRunner bulletml.BulletRunner, _ *bulletml.FireContext) {
b := &Bullet{
runner: bulletRunner,
}
b.x, b.y = bulletRunner.Position()
g.bullets = append(g.bullets, b)
},
// Tell current enemy position
CurrentShootPosition: func() (float64, float64) {
return enemy.x, enemy.y
},
// Tell current player position
CurrentTargetPosition: func() (float64, float64) {
return g.player.x, g.player.y
},
}
// Create new runner
runner, err := bulletml.NewRunner(bml, opts)
if err != nil {
panic(err)
}
// Set runner to enemy
enemy.runner = runner
g.enemies = append(g.enemies, enemy)
}
func (g *Game) Update() error {
// Update enemies
for _, e := range g.enemies {
if err := e.runner.Update(); err != nil {
panic(err)
}
}
// Update bullets
// (g.bullets may be extended in b.runner.Update(), so you should write loop like this.)
for i, n := 0, len(g.bullets); i < n; i++ {
b := g.bullets[i]
if err := b.runner.Update(); err != nil {
panic(err)
}
// Set updated bullet position
b.x, b.y = b.runner.Position()
}
// Keep bullets only not vanished and within the screen
_bullets := g.bullets[:0]
for _, b := range g.bullets {
if !b.runner.Vanished() &&
b.x >= 0 && b.x <= screenWidth && b.y >= 0 && b.y <= screenHeight {
_bullets = append(_bullets, b)
}
}
g.bullets = _bullets
return nil
}
var img = func() *ebiten.Image {
img := ebiten.NewImage(6, 6)
vector.DrawFilledCircle(img, 3, 3, 3, color.RGBA{0xe8, 0x7a, 0x90, 0xff}, true)
return img
}()
func (g *Game) Draw(screen *ebiten.Image) {
screen.Fill(color.RGBA{0xf5, 0xf5, 0xf5, 0xff})
for _, b := range g.bullets {
opts := &ebiten.DrawImageOptions{}
opts.GeoM.Translate(b.x-3, b.y-3)
screen.DrawImage(img, opts)
}
}
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
return screenWidth, screenHeight
}
func main() {
ebiten.SetWindowSize(screenWidth, screenHeight)
game := &Game{
player: &Player{x: screenWidth / 2, y: screenHeight - 100},
}
game.addEnemy(170, 150)
game.addEnemy(screenWidth-170, 150)
if err := ebiten.RunGame(game); err != nil {
panic(err)
}
}
This library contains some extended features of BulletML specifications.
These features are not standard specifications, so BulletML sources which contain them would not work on other BulletML libraries.
You can use loop variables in <repeat>
elements.
$loop.index
- Zero-based loop index
<repeat>
<times>10</times>
<action>
<fire>
<speed>1 + $loop.index</speed>
<bullet />
</fire>
</action>
</repeat>
$direction
- Current bullet direction
$speed
- Current bullet speed
<action>
<changeDirection>
<term>1</term>
<direction type="aim">10 * $rand - 5</direction>
</changeDirection>
<changeSpeed>
<term>1</term>
<speed type="sequence">2 * $rand</speed>
</changeSpeed>
<wait>1</wait>
<fireRef label="fire">
<param>$direction</param> <!-- $direction is the result of `<direction type="aim">10 * $rand - 5</direction>` -->
<param>$speed</param> <!-- $speed is the result of `<speed type="sequence">2 * $rand</speed>` -->
</fireRef>
</action>
You can use these functions in expressions.
sin
cos
Important
sin
andcos
interprets the argument as degrees, not radian.
<direction>sin($loop.index * 180 / 3.14)</direction>