Requirements

You will need:

Background

The visuals of games are built from a bunch of 3D objects in a tree hierarchy. The root of that tree is an instance of your main Game class, which represents the table. Objects can contain other child objects, these children will be drawn when the parent object is drawn.

For example, let’s see how this card game is structured:

Card game example

The tree will look something like this:

Game
├─ Zone         (Table Center)
|  ├─ Deck      (Draw Pile)
|  |  ├─ Card   (Face down)
|  ├─ Deck      (Discard Pile)
|  |  ├─ Card   (7 of Spades)
|  ├─ Hand      (Shop)
|     ├─ Card   (9 of Hearts)
|     ├─ Card   (4 of Spades)
|     ├─ Card   (4 of Diamonds)
├─ Zone         (Player 1)
|  ├─ Hand      (Player Hand)
|     ├─ Card   (Jack of Spades)
|     ├─ Card   (8 of Hearts)
|     ├─ ...
├─ ...

So the Game object contains a bunch of Zone objects, each Zone contains decks or hands of cards, which in turn contain individual Card objects. The tree of your game can be structured in whichever way makes sense to you.

Display Decorator

We’ve got a cool game with the below code. CardGame has an aceOfSpades property, that will hold a single Ace of Spades card.

import * as bge from "bge-core";

// ...

export class CardGame extends bge.Game<Player> {

    readonly aceOfSpades = PlayingCard.create(CardSuit.Spade, CardValue.Ace);

    // ...
}

Running it, we just get an empty table :’(

What a boring game

We need to add the card as a child of the game, like this:

@bge.display()
readonly aceOfSpades = PlayingCard.create(CardSuit.Spade, CardValue.Ace);

Now we’re talking :D

Now we're talking!

Adding @bge.display() above a property will make whatever’s stored in that property be visible, assuming the containing object is also visible. This is only valid on properties inside a class definition, meaning you can’t use it on variables in a method body.

It even works fine on properties with a get method:

@bge.display()
get aceOfSpades() {
    return PlayingCard.create(CardSuit.Spade, CardValue.Ace);
}

The get method will be evaluated each time the object is displayed to a player.

You can display anything extending bge.GameObject, string values, number values, or arrays of those types.

Here’s a string being displayed:

@bge.display()
example: string = "Hello world!";

Hello world!

Dynamic Children

The display decorator isn’t always a good fit, like if you want to add or remove objects arbitrarily during the game.

Here’s another way to add children, given a parent object:

parent.children.add(child);

Later on, you can remove that child object from being displayed with:

parent.children.remove(child);

This way of displaything things is a little more restrictive, because you can’t use it with plain string or number values. You can, however, use it with a function that returns those types:

parent.children.add(function () { return "Hello world!"; });

To go into a bit of detail, this is actually what the bge.display() decorator is doing behind the scenes. It’s calling parent.children.add(...) for each property that was decorated when the parent object is constructed.

You can manually trigger this behaviour like below, allowing you to add all the decorated properties from a different object to a given parent:

parent.children.addProperties(otherObject);

Display Options

You can provide an optional IDisplayOptions object to configure how children are displayed.

With the decorator you use it like this:

@bge.display({
    // options here
})

You can also provide a function that will be called when displaying the object to dynamically generate the display options:

@bge.display(function (this: ParentType, ctx: bge.RenderContext, value: PlayingCard) {
    return {
        // options here
    };
})
example: PlayingCard;

This lets you change the options based on the parent object’s state (this), the render context (ctx, which includes which player the object is being drawn for), and the current value of the displayed property.

When adding children dynamically, you can provide options like this:

parent.children.add(child, {
    // options here
});

Later on, you can modify those options with this:

const options = parent.children.getOptions(child);
// Modify options here

Common Options

position

You can change the position of a child object like this:

@bge.display({ position: { x: -4 } })
card1 = PlayingCard.create(CardSuit.Diamond, CardValue.Four);

@bge.display({ position: { x: 4, y: -1 } })
card2 = PlayingCard.create(CardSuit.Club, CardValue.Queen);

Distances are measured in centimeters. The X axis increases to the right on the table, Y axis increases as you go forwards on the table, and Z increases as you lift off the table surface.

Score

Positions are relative to the parent object, so the axes I described above will be rotated according to the parent’s rotation.

rotation

Rotations can be set with rotation:

@bge.display({ rotation: bge.Rotation.z(45) })
card = PlayingCard.create(CardSuit.Club, CardValue.Queen);

Angles are given in degrees, and in this example we’re rotating around the Z axis (yaw). You can also rotate around the other axes as a way to put objects on their backs or sides.

Rotated card

Rotation, like position, is relative to the parent object.

hiddenFor / revealedFor

Some objects, like cards, can have their identities obscured when in a “hidden” state. The opposite of hidden is “revealed”, meaning the identity is visible.

You can use the hiddenFor and revealedFor display options to choose which players an object is hidden or revealed for:

this.children.add(card1, {
    position: { x: -4 },
    revealedFor: [ this.players[0] ]
});

this.children.add(card2, {
    position: { x: 4 },
    hiddenFor: [ this.players[0] ]
});

The first card is revealed only to Player 1, so all other players will see a generic valueless card in its place. The second card is only hidden to Player 1, so all other players can see it fine.

Hidden and revealed

These options will trickle down to any child objects, so for example setting them on a Hand of cards will cause each contained Card to be hidden for the same set of players.

Since you need to actually provide Player instances with these options, you’ll often use them with the overload of @bge.display(...) that accepts a function:

export class Player extends bge.Player {
    
    @bge.display(function (this: Player) { return {
        revealedFor: [this]
    } })
    readonly hand = new bge.Hand(PlayingCard, HAND_WIDTH_CM);
}

Remember to manually specify the type of the this parameter here to make TypeScript happy. It needs to match the type of the containing class as in this example.

visibleFor / invisibleFor

Similarly, you can make objects selectively invisible to a set of players with visibleFor and invisibleFor:

this.children.add(card1, {
    position: { x: -4 },
    visibleFor: [ this.players[0] ]
});

this.children.add(card2, {
    position: { x: 4 },
    invisibleFor: [ this.players[0] ]
});

From Player 1’s perspective, only card1 can be seen. All other players see only card2.

Invisible card

label

When displaying numbers, the label option is often useful:

@bge.display({ label: "Score" })
score: number = 123;

Score

fontColor

For number or string values, you can change the color with fontColor:

@bge.display({ fontColor: bge.Color.parse("#ff00ff") })
score: number = 123;

Font color

fontScale

Here’s another one specific to text, fontScale:

@bge.display({ fontScale: 0.5, position: { y: 3 } })
smol: string = "im smol";

@bge.display({ fontScale: 2, position: { y: -2 } })
big: string = "I'M BIG";

By default text uses fontScale: 1, so the value you give is relative to that size.

Font scale