Skip to content

Flap ​

Build a tiny flyer: gravity pulls the bird down, a flap pops it up, column pairs slide in from the right. Slip through a gap to score. Hit anything and the run is over.

The recipes on High-level scripting (Update, [Property], Game.SetNumber, Time.Wait) are the pieces. This page puts them together.

One coordinate quirk before you start: stage +Y is down, so a flap sets velocity up (negative Y). It feels backwards once, then it is just how Opal works.

What you will have ​

PieceRole
BirdDynamic body, locked on X, flaps on a button
Column prefabKinematic pair that scrolls left and scores once
GateSpawns a top and bottom column on an interval
Floor / ceilingStatic colliders so leaving the view is a crash
score / playingGameState the HUD and scripts share

Three OpalScript classes. The HUD is a Text label bound to GameState score — no extra script.

1. Project and scene ​

Fresh project, one scene. Default stage is 800×600 — keep that size in mind for spawn positions later.

Variables — two fields the HUD and the scripts will share:

NameTypeDefaultLifetime
scoreNumber0Session
playingTrue/FalsetrueSession

Input panel — add a button action named Flap. Bind Space. Add a mouse or pointer press if you want tap-to-flap.

2. Floor, ceiling, bird ​

In Arrange, give the bird something to hit:

  1. A wide box on the bottom of the view. Collider 2D only (static floor). Name it Floor.
  2. A wide box on the top. Collider 2D only. Name it Ceiling.
  3. A small sprite on the left third, around (160, 300). Name it Bird.

On Bird add:

ComponentSettings
Collider 2DCircle, not a sensor, sized to the sprite
Rigid Body 2DDynamic, Gravity Scale 1, Lock X on, Lock Rotation on
Bird (script below)Flap Speed 420 to start

Scripts → New Behavior, paste Bird, Save & Apply, then attach it.

csharp
class Bird : Component {
    [Property] float FlapSpeed = 420;

    void Start() {
        Game.SetNumber("score", 0);
        Game.SetBool("playing", true);
    }

    void Update(float dt) {
        if (!Game.GetBool("playing")) return;
        if (Input.Pressed("Flap")) Boost();
    }

    public void Boost() {
        RigidBody2D body = GetComponent<RigidBody2D>();
        if (body != null) body.SetVelocity(new Vector2(0, -FlapSpeed));
    }

    void OnCollisionStart(Entity other) {
        Crash();
    }

    void Crash() {
        if (!Game.GetBool("playing")) return;
        Game.SetBool("playing", false);
        RigidBody2D body = GetComponent<RigidBody2D>();
        if (body != null) body.SetVelocity(Vector2.Zero);
        foreach (Entity entity in Scene.FindWith<Column>()) {
            RigidBody2D column = entity.GetComponent<RigidBody2D>();
            if (column != null) column.SetVelocity(Vector2.Zero);
        }
    }
}

Press Play and mash Flap. The bird should hop against gravity and die on the floor. If it does not hop, check that Rigid Body 2D is on Bird — GetComponent<RigidBody2D>() is null until that component is attached. Native bodies finish initializing before Start.

3. Column prefab ​

The hazards are one prefab, spawned twice (top and bottom). Make a tall rectangle — about 80×420 — with a solid color or sprite. Name it Column.

ComponentSettings
Collider 2DBox, not a sensor
Rigid Body 2DKinematic, Gravity Scale 0
Column (script below)Speed 180, Score X 160, Score Below Y 300, Despawn X -120
csharp
class Column : Component {
    [Property] float Speed = 180;
    [Property] float ScoreX = 160;
    [Property] float ScoreBelowY = 300;
    [Property] float DespawnX = -120;
    bool scored;

    void Start() {
        RigidBody2D body = GetComponent<RigidBody2D>();
        if (body != null) {
            body.SetVelocityKinematic();
            body.SetVelocity(new Vector2(-Speed, 0));
        }
    }

    void Update(float dt) {
        if (!scored && Transform.Position.X < ScoreX && Transform.Position.Y > ScoreBelowY) {
            scored = true;
            if (Game.GetBool("playing")) {
                Game.SetNumber("score", Game.GetNumber("score") + 1);
            }
        }
        if (Transform.Position.X < DespawnX) {
            Scene.Destroy(Self);
            return;
        }
    }
}

Save it as a prefab / blueprint (Column). Then delete the scene instance — Gate will spawn the copies, so you do not want a leftover column sitting on the bird.

Score Below Y is how only the lower column awards a point (stage Y grows downward). Set it near mid-screen (300 on an 800×600 stage). Score X should match the bird’s X.

SetVelocityKinematic plus SetVelocity is the moving-platform pattern: the column slides left and still collides.

4. Gate ​

The Gate is an empty object that just runs a loop: wait, spawn a pair, repeat.

Empty object named Gate. Attach Gate. Assign the Column blueprint to Prefab (the field is named Prefab, not Column — that name would clash with the class).

csharp
class Gate : Component {
    [Property] Asset<blueprint> Prefab;
    [Property] float Interval = 1.6;
    [Property] float SpawnX = 420;
    [Property] float Gap = 180;
    [Property] float ColumnHeight = 420;

    async void Start() {
        while (true) {
            await Time.Wait(Interval);
            if (!Game.GetBool("playing")) continue;
            if (Prefab == null) continue;
            float center = Math.Random(180, 420);
            float offset = Gap * 0.5 + ColumnHeight * 0.5;
            Scene.Spawn(Prefab, new Vector2(SpawnX, center - offset));
            Scene.Spawn(Prefab, new Vector2(SpawnX, center + offset));
        }
    }
}

ColumnHeight must match the prefab’s height so the Gap is a real opening. SpawnX should sit just off the right of the view. Assigning the blueprint on Prefab is also what tells export to include it.

await Time.Wait inside async void Start is the same pattern as the wait-and-turn recipe. while (true) is safe here because every pass waits; a loop with no wait hits the 10,000-operation cap.

5. HUD ​

Time for the score. Open the UI workspace and add a Text element. Park it somewhere readable — top-left is fine.

Select it. In the Inspector, Data bindings is the card that keeps the label in sync with score.

On Text:

  • Source → GameState — that is the project variable you declared. (Component is for a field on a scene object, like health on the player.)
  • Value → score
  • Format → Integer (rounded) so you do not see 1.0
  • Template → Score {value}

{value} is a placeholder the picker fills in — type whatever you want around it. Leave Visible and Opacity on None for now.

Play, pass a gap, and the number should tick. Same field the scripts write with Game.SetNumber("score", …). More on the picker: UI and HUD.

6. Play ​

Hit Play. Flap through a gap. The score should tick once per pair when the lower column slides past the bird.

Clip the floor, the ceiling, or a column and Bird.Crash runs: playing flips false, columns freeze, Gate stops opening new pairs.

Stop, then Play to try again — Start zeros score and sets playing back.

Tuning ​

FeelChange
Heavy birdLower Flap Speed, or raise the body’s gravity scale
Tight gapsLower Gate Gap, keep Column Height in sync with the sprite
Faster roundRaise Column Speed and lower Gate Interval
Score too early / lateColumn Score X
Both columns scoringScore Below Y is too small — raise it toward mid-screen
Columns spawn on top of the birdRaise Spawn X
Empty gap (no hitboxes)Prefab needs Collider 2D and a kinematic Rigid Body 2D

Built-in Health is unused on purpose: this is a crash, not an HP bar. You can still call GetComponent<Health>() on other objects in a different project.

What this lesson used ​

APIWhere
Input.PressedBird flap
RigidBody2D.SetVelocityFlap and freeze
OnCollisionStartCrash
Scene.FindWith<Column>()Stop every live column
Scene.SpawnGate
Math.RandomGap height
Time.WaitSpawn interval
Game.GetNumber / SetNumber, GetBool / SetBoolScore and run state

Class pages: Input, RigidBody2D, Scene, Time, Game, Math.