bulletml

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 18, 2023 License: MIT Imports: 14 Imported by: 3

README

go-bulletml

A Go implementation of BulletML

Sample

Demo

You can play a BulletML web simulator from here.

Simulator

Usage

API Reference

https://pkg.go.dev/github.com/tsujio/go-bulletml

1. Write BulletML source

The BulletML specifications are here

Important

go-bulletml and many BulletML libraries (and also the original) run top-level <action> elements that have label 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>

2. Load BulletML data in your game program

f, err := os.Open("bulletml.xml")
if err != nil {
	panic(err)
}
defer f.Close()

bml, err := bulletml.Load(f)
if err != nil {
	panic(err)
}

3. Create new runner

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

4. Call runner's Update method in every loop

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

Full source code

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

Extensions of BulletML Specifications

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.

Loop variables

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>

Bullet states

  • $direction
    • Current bullet direction
  • $speed
    • Current bullet speed
<action>
    <changeDirection>
        <term>1</term>
        <direction type="absolute">360 * $rand</direction>
    </changeDirection>
    <changeSpeed>
        <term>1</term>
        <speed type="absolute">5 * $rand</speed>
    </changeSpeed>
    <wait>1</wait>
    <fireRef label="fire">
        <param>$direction</param> <!-- $direction is the result of `360 * $rand` -->
        <param>$speed</param> <!-- $direction is the result of `5 * $rand` -->
    </fireRef>
</action>

Math functions

You can use these functions in expressions.

  • sin
  • cos

sin and cos interprets the argument as degrees, not radian.

<direction>sin($loop.index * 180 / 3.14)</direction>

References

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Accel

type Accel struct {
	XMLName    xml.Name    `xml:"accel"`
	Horizontal *Horizontal `xml:"horizontal,omitempty"`
	Vertical   *Vertical   `xml:"vertical,omitempty"`
	Term       Term        `xml:"term"`
	Comment    string      `xml:",comment"`
	// contains filtered or unexported fields
}

type Action

type Action struct {
	XMLName  xml.Name `xml:"action"`
	Label    string   `xml:"label,attr,omitempty"`
	Commands []any    `xml:",any"`
	Comment  string   `xml:",comment"`
	// contains filtered or unexported fields
}

func (*Action) UnmarshalXML

func (a *Action) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error

type ActionRef

type ActionRef struct {
	XMLName xml.Name `xml:"actionRef"`
	Label   string   `xml:"label,attr"`
	Params  []Param  `xml:"param"`
	Comment string   `xml:",comment"`
	// contains filtered or unexported fields
}

type Bullet

type Bullet struct {
	XMLName      xml.Name   `xml:"bullet"`
	Label        string     `xml:"label,attr,omitempty"`
	Direction    *Direction `xml:"direction,omitempty"`
	Speed        *Speed     `xml:"speed,omitempty"`
	ActionOrRefs []any      `xml:",any"`
	Comment      string     `xml:",comment"`
	// contains filtered or unexported fields
}

func (*Bullet) UnmarshalXML

func (b *Bullet) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error

type BulletML

type BulletML struct {
	XMLName xml.Name     `xml:"bulletml"`
	Type    BulletMLType `xml:"type,attr"`
	Bullets []Bullet     `xml:"bullet"`
	Actions []Action     `xml:"action"`
	Fires   []Fire       `xml:"fire"`
	Comment string       `xml:",comment"`
}

func Load

func Load(src io.Reader) (*BulletML, error)

Load loads data from src and returns BulletML object.

type BulletMLType

type BulletMLType string
const (
	BulletMLTypeNone       BulletMLType = "none"
	BulletMLTypeVertical   BulletMLType = "vertical"
	BulletMLTypeHorizontal BulletMLType = "horizontal"
)

type BulletRef

type BulletRef struct {
	XMLName xml.Name `xml:"bulletRef"`
	Label   string   `xml:"label,attr"`
	Params  []Param  `xml:"param"`
	Comment string   `xml:",comment"`
	// contains filtered or unexported fields
}

type BulletRunner

type BulletRunner interface {
	Runner

	// Position returns the bullet position (x, y).
	Position() (float64, float64)

	// Vanished returns whether the bullet has vanished or not.
	Vanished() bool
}

BulletRunner runs BulletML and updates the state of a bullet.

type ChangeDirection

type ChangeDirection struct {
	XMLName   xml.Name  `xml:"changeDirection"`
	Direction Direction `xml:"direction"`
	Term      Term      `xml:"term"`
	Comment   string    `xml:",comment"`
	// contains filtered or unexported fields
}

type ChangeSpeed

type ChangeSpeed struct {
	XMLName xml.Name `xml:"changeSpeed"`
	Speed   Speed    `xml:"speed"`
	Term    Term     `xml:"term"`
	Comment string   `xml:",comment"`
	// contains filtered or unexported fields
}

type Direction

type Direction struct {
	XMLName xml.Name      `xml:"direction"`
	Type    DirectionType `xml:"type,attr"`
	Expr    string        `xml:",chardata"`
	Comment string        `xml:",comment"`
	// contains filtered or unexported fields
}

type DirectionType

type DirectionType string
const (
	DirectionTypeAim      DirectionType = "aim"
	DirectionTypeAbsolute DirectionType = "absolute"
	DirectionTypeRelative DirectionType = "relative"
	DirectionTypeSequence DirectionType = "sequence"
)

type Fire

type Fire struct {
	XMLName   xml.Name   `xml:"fire"`
	Label     string     `xml:"label,attr,omitempty"`
	Direction *Direction `xml:"direction,omitempty"`
	Speed     *Speed     `xml:"speed,omitempty"`
	Bullet    *Bullet    `xml:"bullet,omitempty"`
	BulletRef *BulletRef `xml:"bulletRef,omitempty"`
	Comment   string     `xml:",comment"`
	// contains filtered or unexported fields
}

type FireContext

type FireContext struct {
	// Fire field is the <fire> element which emits this event.
	Fire *Fire

	// Bullet field is the <bullet> element fired by this event.
	Bullet *Bullet
}

FireContext contains context data of fire.

type FireRef

type FireRef struct {
	XMLName xml.Name `xml:"fireRef"`
	Label   string   `xml:"label,attr"`
	Params  []Param  `xml:"param"`
	Comment string   `xml:",comment"`
	// contains filtered or unexported fields
}

type Horizontal

type Horizontal struct {
	XMLName xml.Name       `xml:"horizontal"`
	Type    HorizontalType `xml:"type,attr"`
	Expr    string         `xml:",chardata"`
	Comment string         `xml:",comment"`
	// contains filtered or unexported fields
}

type HorizontalType

type HorizontalType string
const (
	HorizontalTypeAbsolute HorizontalType = "absolute"
	HorizontalTypeRelative HorizontalType = "relative"
	HorizontalTypeSequence HorizontalType = "sequence"
)

type NewRunnerOptions

type NewRunnerOptions struct {
	// [Required] OnBulletFired is called when a bullet is fired.
	OnBulletFired func(BulletRunner, *FireContext)

	// [Required] CurrentShootPosition tells the runner where the shooter is.
	CurrentShootPosition func() (float64, float64)

	// [Required] CurrentTargetPosition tells the runner where the player is.
	CurrentTargetPosition func() (float64, float64)

	// DefaultBulletSpeed is the default value of bullet speed. 1.0 is used if not specified.
	DefaultBulletSpeed float64

	// Random is used as a random generator in the runner.
	Random *rand.Rand

	// Rank is the value for $rank.
	Rank float64
}

NewRunnerOptions contains options for NewRunner function.

type Param

type Param struct {
	XMLName xml.Name `xml:"param"`
	Expr    string   `xml:",chardata"`
	Comment string   `xml:",comment"`
	// contains filtered or unexported fields
}

type Repeat

type Repeat struct {
	XMLName   xml.Name   `xml:"repeat"`
	Times     Times      `xml:"times"`
	Action    *Action    `xml:"action,omitempty"`
	ActionRef *ActionRef `xml:"actionRef,omitempty"`
	Comment   string     `xml:",comment"`
	// contains filtered or unexported fields
}

type Runner

type Runner interface {
	// Update updates runner state. It should be called in every loop.
	Update() error
}

Runner runs BulletML.

func NewRunner

func NewRunner(bulletML *BulletML, opts *NewRunnerOptions) (Runner, error)

NewRunner creates a new Runner.

type Speed

type Speed struct {
	XMLName xml.Name  `xml:"speed"`
	Type    SpeedType `xml:"type,attr"`
	Expr    string    `xml:",chardata"`
	Comment string    `xml:",comment"`
	// contains filtered or unexported fields
}

type SpeedType

type SpeedType string
const (
	SpeedTypeAbsolute SpeedType = "absolute"
	SpeedTypeRelative SpeedType = "relative"
	SpeedTypeSequence SpeedType = "sequence"
)

type Term

type Term struct {
	XMLName xml.Name `xml:"term"`
	Expr    string   `xml:",chardata"`
	Comment string   `xml:",comment"`
	// contains filtered or unexported fields
}

type Times

type Times struct {
	XMLName xml.Name `xml:"times"`
	Expr    string   `xml:",chardata"`
	Comment string   `xml:",comment"`
	// contains filtered or unexported fields
}

type Vanish

type Vanish struct {
	XMLName xml.Name `xml:"vanish"`
	Comment string   `xml:",comment"`
	// contains filtered or unexported fields
}

type Vertical

type Vertical struct {
	XMLName xml.Name     `xml:"vertical"`
	Type    VerticalType `xml:"type,attr"`
	Expr    string       `xml:",chardata"`
	Comment string       `xml:",comment"`
	// contains filtered or unexported fields
}

type VerticalType

type VerticalType string
const (
	VerticalTypeAbsolute VerticalType = "absolute"
	VerticalTypeRelative VerticalType = "relative"
	VerticalTypeSequence VerticalType = "sequence"
)

type Wait

type Wait struct {
	XMLName xml.Name `xml:"wait"`
	Expr    string   `xml:",chardata"`
	Comment string   `xml:",comment"`
	// contains filtered or unexported fields
}

Directories

Path Synopsis
benchmark module
simulator module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL