BeHavior Language
bhl is a strictly typed programming language specifically tailored for gameplay logic scripting. It combines Behaviour Trees(BT) primitives with familiar imperative coding style.
First time it was presented at the nucl.ai conference in 2016. Here's the presentation slides.
Please note that bhl is in alpha state and currently targets only C# platform. Nonetheless it has been battle tested in the real world projects and heavily used by BIT.GAMES for mobile games development built with Unity.
bhl features
- ANTLR based: C# frontend + C# interpreting backend
- Statically typed
- Built-in support for pseudo parallel code orchestration
- Golang alike defer
- Basic types: float, int, bool, string, enums, arrays, classes
- Supports imperative style control constructs: if/else, while, break, return
- Allows user defined: functions, lambdas, classes
- Supports C# bindings to user types and functions
- Passing arguments to function by ref like in C#
- Multiple returned values like in Golang
- Hot code reload
- Strict control over memory allocations
Quick example
func GoToTarget(Unit u, Unit t) {
NavPath path
defer {
PathRelease(path)
}
paral {
yield while(!IsDead(u) && !IsDead(t) && !IsInRange(u, t))
{
path = FindPathTo(u, t)
Wait(1)
}
{
FollowPath(u, path)
}
}
Code samples
Structs
class Color3 {
float r
float g
float b
}
class Color4 : Color3 {
float a
}
Color4 c = {}
c.r = 0.9
c.g = 0.5
c.b = 0.7
c.a = 1.0
Enums
enum Status {
None = 0
Connecting = 1
Connected = 2
}
Status s = Status.Connected
Generic initializers
class Vec3 {
float x
float y
float z
}
Vec3[] vs = [{x: 10}, {y: 100, z: 100}, {y: 1}]
Passing by ref
Unit FindTarget(Unit self, ref float dist_to_target) {
...
dist_to_target = u.position.Sub(self.position).length
return u
}
float dist_to_target = 0
Unit u = FindTarget(self, ref dist_to_target)
Multiple returned values
Unit,float FindTarget(Unit self) {
...
float dist_to_target = u.position.Sub(self.position).length
return u,dist_to_target
}
Unit u,float dist_to_target = FindTarget(self)
Closures
Unit u = FindTarget()
float distance = 4
u.InjectScript(func() {
paral_all {
PushBack(distance: distance)
Stun(time: 0.4, intensity: 0.15)
}
})
Function pointers
func bool(int) p = func bool(int b) { return b > 1 }
return p(10)
defer support
{
RimColorSet(color: {r: 0.65, a: 1.0}, power: 1.1)
defer { RimColorSet(color: {a: 0}, power: 0) }
...
}
Pseudo parallel code execution
func Attack(Unit u) {
Unit t = TargetInRange(u)
Check(t != null)
paral_all {
PlayAnim(u, trigger: "Attack")
SoundPlay(u, sound: "Swoosh")
seq {
WaitAnimEvent(u, event: "Hit")
SoundPlay(u, sound: "Damage")
HitTarget(u, t, damage: RandRange(1,16))
}
}
Example of some unit's top behavior
func Selector([]func bool() fns) {
foreach(func bool() fn in fns) {
if(!fn()) {
continue
} else {
break
}
}
}
func UnitScript(Unit u) {
while(true) {
paral {
WaitStateChanged(u)
Selector(
[
func bool() { return FindTarget(u) },
func bool() { return AttackTarget(u) },
func bool() { return Idle(u) }
]
)
}
yield()
}
}
Architecture
bhl utilizes a standard interpreter architecture with a frontend and a backend. Frontend is responsible for reading input files, static type checking and bytecode generation. Binary bytecode is post-processed and optimized in a separate stage. Processed byte code can be used by the backend. Backend is a interpreter responsible for runtime bytecode evaluation. Backend can be nicely integrated with Unity.
Frontend
In order to use the frontend you can use the bhl tool which ships with the code. See the quick build example below for instructions.
Backend
Before using the backend you have to compile the bhl_back.dll and somehow integrate it into your build pipeline. See the quick build example below for instructions.
Quick build example
Currently bhl assumes that you have mono installed and its binaries are in your PATH.
In the example directory you can find a simple illustration of gluing together frontend and backend.
Just try running run.sh script:
cd example && ./run.sh
This example executes the following simple script
Unit starts...
No target in range
Idling 3 sec...
State changed!
Idle interrupted!
Found new target 703! Approaching it.
Attacking target 703
Target 703 is dead!
Found new target 666! Approaching it.
State changed!
Found new target 902! Approaching it.
...
Please note that while bhl works fine under Windows the example assumes you are using *nix platform.
Unity engine integration
The example script has also a special Unity compatibility mode. It illustrates how you can build a bhl backend dll (bhl_back.dll) for Unity. After that you can put it into Assets/Plugins directory and use bhl for your Unity game development. You can run the example script in this mode just as follows:
cd example && ./run.sh -unity
Building
bhl comes with its own simple build tool bhl. bhl tool is written in C# and should work just fine both on *nix and Windows platforms.
It allows you to build frontend dll, backend dll, compile bhl sources into a binary, run unit tests etc.
You can view all available build tasks with the following command:
$ bhl help
Tests
For now there is no any documentation for bhl except presentation slides. However, there are many unit tests which cover all bhl features.
You can run unit tests by executing the following command:
$ bhl test
Roadmap
Version 3.0
- Generics support
- More optimal byte code
- More optimal runtime memory storage layout
Version 2.0
Byte code optimizationMore optimal executor (VM)Better runtime errors reportingMore robust type systemUser class methodsInterfaces supportNamespaces supportPolymorphic class methodsNested classesNested in classes enumsStatic class members supportVariadic function argumentsMaps support- Implicit variable types using 'var'
- Debugger support
- LSP integration
Version 1.0
ref semantics similar to C#Generic functors supportGeneric initializersMultiple return values supportwhile syntax sugar: for(...) {} supportwhile syntax sugar: foreach(...) {} supportTernary operator supportUser defined structsUser defined enumsPostfix increment/decrement