ScopeBee

Documentation

The Beef language

ScopeBee games are written in Beef: a compiled language with the syntax of C# and the control of C++.

Made by a game developer

Beef was started by Brian Fiete, a co-founder of PopCap Games — the studio behind Bejeweled, Zuma, Peggle and Plants vs. Zombies — who worked there until 2011. He has worked on Beef more or less full time since September 2014; the first public release came out in January 2020.

The language was built for the work he knew: its stated goal is a fluid development experience for high-performance real-time applications such as games, low-level enough to write an engine in and comfortable enough to write the game on top of it.

The language in brief

  • Statically and strongly typed.
  • Compiled ahead of time to native code: no virtual machine, no JIT, a minimal runtime.
  • Manual memory management: no garbage collector and no reference counting. Objects are created with new and released with delete, or placed on the stack with scope and released at the end of it.
  • First-class custom allocators.
  • Debug builds catch memory leaks in real time and guard against use-after-free and double deletion; release builds can turn those checks off.
  • No exceptions: errors are returned as Result values.
  • Value types and reference types, generics, interfaces, opt-in reflection and code executed at compile time.
  • Direct interop with C and C++ libraries.
  • Open source, MIT licence.

It reads like C#

The most important thing to know about Beef: its syntax and many of its semantics come straight from C#. Classes, structs, interfaces, properties, generics, namespaces, attributes — they are written the same way, and code often carries over line for line.

For a developer who knows Unity that means no new language to learn from scratch. What changes is not how the code looks but who owns the memory — the comparison below shows both sides.

Performance and platforms

Beef compiles to machine code through LLVM, the same backend behind Clang and Rust. It gets the whole of the LLVM optimizer, including link-time optimization, and a frame never stalls on garbage collection because there is none.

Optimization can be set per type or per method, so hot code runs at full speed while the rest of the game stays easy to debug.

LLVM is also what makes Beef cross-platform: it builds natively for Windows, Linux and macOS, and can target any architecture LLVM generates code for — iOS and Android among them. That is how ScopeBee takes the same Beef code to WebAssembly and into the browser.

C# and Beef side by side

What is the same

A class with a property, fields, methods, inheritance and an override. The two listings are character for character the same.

C#

public class Player : Unit, IDamageable
{
    public int Health { get; set; } = 100;
    private float _speed = 4.5f;

    public void TakeDamage(int amount)
    {
        Health -= amount;
        if (Health <= 0)
            Die();
    }

    protected override void Die()
    {
        base.Die();
        _speed = 0;
    }
}

Beef

public class Player : Unit, IDamageable
{
    public int Health { get; set; } = 100;
    private float _speed = 4.5f;

    public void TakeDamage(int amount)
    {
        Health -= amount;
        if (Health <= 0)
            Die();
    }

    protected override void Die()
    {
        base.Die();
        _speed = 0;
    }
}

Interfaces, structs and enums are declared the same way too.

C#

public interface IDamageable
{
    void TakeDamage(int amount);
}

public struct Hit
{
    public Vector2 Point;
    public int Damage;
}

public enum State { Idle, Run, Jump }

Beef

public interface IDamageable
{
    void TakeDamage(int amount);
}

public struct Hit
{
    public Vector2 Point;
    public int Damage;
}

public enum State { Idle, Run, Jump }

So are generics and their constraints.

C#

public class Pool<T> where T : Unit
{
    private List<T> _free;
    private List<T> _used;
}

Beef

public class Pool<T> where T : Unit
{
    private List<T> _free;
    private List<T> _used;
}

Expressions, operators, loops and the standard maths.

C#

int score = combo > 1 ? points * combo : points;

while (lives > 0 && !gameOver)
{
    lives--;
    score += bonus;
}

for (int i = 0; i < count; i++)
    Spawn(i);

float speed = Math.Min(_speed * 2, maxSpeed);

Beef

int score = combo > 1 ? points * combo : points;

while (lives > 0 && !gameOver)
{
    lives--;
    score += bonus;
}

for (int i = 0; i < count; i++)
    Spawn(i);

float speed = Math.Min(_speed * 2, maxSpeed);

Casts with as, and the null-conditional and null-coalescing operators.

C#

Enemy enemy = target as Enemy;
enemy?.TakeDamage(10);
Unit focus = enemy ?? player;

Beef

Enemy enemy = target as Enemy;
enemy?.TakeDamage(10);
Unit focus = enemy ?? player;

out and ref parameters, written at the call site as well.

C#

bool TryGetHit(out Hit hit) { ... }
void Apply(ref Hit hit) { ... }

Hit hit;
if (TryGetHit(out hit))
    Apply(ref hit);

Beef

bool TryGetHit(out Hit hit) { ... }
void Apply(ref Hit hit) { ... }

Hit hit;
if (TryGetHit(out hit))
    Apply(ref hit);

Namespaces, using, constants and static members.

C#

using System;

namespace Game
{
    public static class Rules
    {
        public const int MaxLives = 3;
        public static float Gravity = 9.81f;
    }
}

Beef

using System;

namespace Game
{
    public static class Rules
    {
        public const int MaxLives = 3;
        public static float Gravity = 9.81f;
    }
}

Where they differ

Memory. Nothing is collected: scope frees at the end of the block, new is paired with delete, and defer runs it on the way out.

C#

var list = new List<Enemy>();
var boss = new Boss();

Beef

let list = scope List<Enemy>();
let boss = new Boss();
defer delete boss;

Constructors and destructors are called this and ~this.

C#

public Level(int number) { }
~Level() { }

Beef

public this(int number) { }
public ~this() { }

foreach is spelled for … in.

C#

foreach (var e in enemies)
    e.Update();

Beef

for (let e in enemies)
    e.Update();

A case needs no break: it never falls through unless you write fallthrough.

C#

case State.Idle:
    Rest();
    break;

Beef

case .Idle:
    Rest();

No exceptions: a method that can fail returns a Result, and the caller matches on it.

C#

try { Load(path); }
catch (IOException e) { Log(e); }

Beef

if (Load(path) case .Err(let e))
    Log(e);

A String is a mutable buffer the caller owns, and formatting uses {} placeholders.

C#

string hud = $"HP: {hp}";

Beef

let hud = scope String();
hud.AppendF("HP: {}", hp);

Known limitations

No async/await or coroutines

Beef has neither async/await nor coroutines. Asked whether there are plans for them, the language's official FAQ answers:

“These will be considered for a future version of BeefLang.” Beef FAQ