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
| Piece | Role |
|---|---|
| Bird | Dynamic body, locked on X, flaps on a button |
| Column prefab | Kinematic pair that scrolls left and scores once |
| Gate | Spawns a top and bottom column on an interval |
| Floor / ceiling | Static colliders so leaving the view is a crash |
| score / playing | GameState 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:
| Name | Type | Default | Lifetime |
|---|---|---|---|
score | Number | 0 | Session |
playing | True/False | true | Session |
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:
- A wide box on the bottom of the view. Collider 2D only (static floor). Name it
Floor. - A wide box on the top. Collider 2D only. Name it
Ceiling. - A small sprite on the left third, around
(160, 300). Name itBird.
On Bird add:
| Component | Settings |
|---|---|
| Collider 2D | Circle, not a sensor, sized to the sprite |
| Rigid Body 2D | Dynamic, 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.
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.
| Component | Settings |
|---|---|
| Collider 2D | Box, not a sensor |
| Rigid Body 2D | Kinematic, Gravity Scale 0 |
| Column (script below) | Speed 180, Score X 160, Score Below Y 300, Despawn X -120 |
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).
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
| Feel | Change |
|---|---|
| Heavy bird | Lower Flap Speed, or raise the body’s gravity scale |
| Tight gaps | Lower Gate Gap, keep Column Height in sync with the sprite |
| Faster round | Raise Column Speed and lower Gate Interval |
| Score too early / late | Column Score X |
| Both columns scoring | Score Below Y is too small — raise it toward mid-screen |
| Columns spawn on top of the bird | Raise 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
| API | Where |
|---|---|
Input.Pressed | Bird flap |
RigidBody2D.SetVelocity | Flap and freeze |
OnCollisionStart | Crash |
Scene.FindWith<Column>() | Stop every live column |
Scene.Spawn | Gate |
Math.Random | Gap height |
Time.Wait | Spawn interval |
Game.GetNumber / SetNumber, GetBool / SetBool | Score and run state |
Class pages: Input, RigidBody2D, Scene, Time, Game, Math.
Related
- High-level scripting
- Your first component
- Physics and collisions
- Prefabs and spawning
- Input and controls
- Low-level scripting — when you want the full native and task contracts