Sample Scripts
This page contains a collection of sample scripts demoing a good variety of features available in Blip.
This list is not exaustive but is a great start to learn Blip, or even later on when looking for a reference when implementing one new kind of feature.
Script Structure
Here's an example showing the overall structure of a Blip script.
It's important to note that for multiplayer games, the execution of the script is distributed between connected clients and the server.
Keeping everything in one place makes it easier to reason about game logic.
-- A single Blip script is by default executed on the client AND server.
-- A variable defined at the root level (local or global)
-- will exist on both client and server:
local someVariable = "hello"
-- Client properties will only be considered on the client:
function dropPlayer()
Player:SetParent(World)
Player.Position = {0, 40, 0}
end
Client.OnStart = function()
print("game started!")
dropPlayer()
Camera.Behavior = {
positionTarget = Player, -- camera goes to that position (or position of given object)
positionTargetOffset = { 0, 14, 0 }, -- applying offset to the target position
positionTargetBackoffDistance = 40, -- camera then tries to backoff that distance, considering collision
positionTargetMinBackoffDistance = 20,
positionTargetMaxBackoffDistance = 100,
rotationTarget = Player.Head, -- camera rotates to that rotation (or rotation of given object)
rigidity = 0.5, -- how fast the camera moves to the target
collidesWithGroups = nil, -- camera will not go through objects in these groups
}
end
Client.Action1 = function()
if Player.IsOnGround then
Player.Velocity.Y = 100
end
end
Client.Tick = function(dt)
-- tick at each frame, dt: time since last tick
if Player.Position.Y < -100 then
-- respawn player when falling off
dropPlayer()
end
end
Client.DidReceiveEvent = function(event)
if event.message ~= nil then
print("received: " .. event.message)
end
end
-- Server properties will only be considered on the server:
Server.OnStart = function()
visits = 0
end
Server.OnPlayerJoin = function(p)
-- Player `p` just joined the game, let's send a message:
visits += 1
local e = Event()
e.message = "Hello " .. p.Username .. "! You are the " .. visits .. "th player to join!"
e:SendTo(p)
end
User Interface
How to build a simple User Interface
Here's an example showing how to create a user interface with buttons, texts and frame containers. It's highly recommended to use a module like uikit to build the UI. uikit is used to build all systems UI in Blip.
Modules = {
uikit = "uikit" -- imports `uikit` module, exposed globally as `uikit`
}
Client.OnStart = function()
-- TEXTS
local text = uikit:createText("Hello, world!")
-- All uikit components are bottom left anchored
-- and positioned relative to the bottom left corner of their parent.
-- By default, the parent is the screen.
text.pos = {10, 10}
-- Text components accept some style properties.
-- Here's an example with a text to display a score at the top right corner of the screen:
local scoreValue = 0
local score = uikit:createText(string.format("Score: %d", scoreValue), {
color = Color(235, 203, 139),
size = "big", -- options: "small", "default" (default), "big"
align = "right", -- options: "left" (default), "center", "right"
outline = 1.0,
outlineColor = Color(122, 172, 187),
bold = true,
})
-- to ensure responsiveness, it's a good practice to position the component within its `parentDidResize` callback:
score.parentDidResize = function(self)
self.pos = {
-- Screen.SafeArea helps considering device special areas (e.g. notch on iPhone)
-- Screen.SafeArea.Bottom, Top, Left & Right represent positive distances from each edge.
Screen.Width - Screen.SafeArea.Right - self.Width - 10,
Screen.Height - Screen.SafeArea.Top - self.Height - 10
}
end
score:parentDidResize() -- call once manually to set initial position
-- BUTTONS
-- here's how to create a button with default style:
local button = uikit:button({
content = "Increase score!",
})
-- center the button
button.parentDidResize = function(self)
self.pos = {
Screen.Width * 0.5 - self.Width * 0.5,
Screen.Height * 0.5 - self.Height * 0.5
}
end
button:parentDidResize() -- call once manually to set initial position
-- Add a callback to the button
button.onRelease = function()
scoreValue += 1
score.Text = string.format("Score: %d", scoreValue) -- update text component with new score
score:parentDidResize() -- update position considering new score text width
end
end
Physics and Collisions
Simple physics and pointer raycasting
Here's an example of a world showcasing simple physics and pointer raycasting.
Modules = {
controls = "controls",
}
-- Dev.DisplayColliders = true -- uncomment to display colliders
local collisionGroupMap = CollisionGroups(1) -- collision group for Objects that are part of the map
local collisionGroupPlayers = CollisionGroups(2) -- collision group for Players
local collisionGroupObjects = CollisionGroups(3) -- collision group for Objects that are not part of the map
local collisionGroupTomatoes = CollisionGroups(4) -- collision group for tomatoes
-- declaring local variables upstream so that they can be captured as upvalues,
-- values are assigned in Client.OnStart if found in the World.
local trampoline = nil
local tomato = nil
local cat = nil
local tomatoes = {} -- recycle pool for tomatoes
Client.OnStart = function()
World:Recurse(function(o)
if o.Name == "trampoline" then
trampoline = o
-- bounciness == 1: maintain velocity
-- bounciness > 1: increase velocity
-- bounciness < 1: decrease velocity
trampoline.Bounciness = 1.1
elseif o.Name == "tomato" then
tomato = o
tomato.CollisionGroups = collisionGroupTomatoes
tomato.CollidesWithGroups = collisionGroupMap + collisionGroupObjects
tomato.Physics = PhysicsMode.Dynamic
tomato:RemoveFromParent() -- tomato is used as a projectile, we don't need it in the scene
elseif o.Name == "cat" then
cat = o
cat.Physics = PhysicsMode.Dynamic
-- cat can collide with map and other objects like the trampoline
cat.CollisionGroups = collisionGroupObjects
cat.CollidesWithGroups = collisionGroupMap + collisionGroupObjects
-- creating a trigger box to detect when the player is close to the cat,
-- when that happens, the cat will look at the player.
local catTrigger = Object()
catTrigger.Physics = PhysicsMode.Trigger
catTrigger.CollisionGroups = nil
catTrigger.CollidesWithGroups = collisionGroupPlayers
catTrigger:SetParent(cat)
local triggerBoxSize = cat.CollisionBox.Size * 4
local halfSize = triggerBoxSize * 0.5
catTrigger.CollisionBox = Box(
Number3(-halfSize.Width, 0, -halfSize.Depth),
Number3(halfSize.Width, halfSize.Height * 2, halfSize.Depth)
)
local lookAtPlayerListener
catTrigger.OnCollisionBegin = function(o, other)
if other == Player then
if lookAtPlayerListener == nil then
-- creating a tick listener to keep updating the cat's rotation
lookAtPlayerListener = LocalEvent:Listen(LocalEvent.Name.Tick, function(dt)
local p1 = cat.Position:Copy() p1.Y = 0
local p2 = Player.Position:Copy() p2.Y = 0
cat.Rotation:SetLookRotation(p2 - p1)
end)
end
end
end
catTrigger.OnCollisionEnd = function(o, other)
if other == Player then
if lookAtPlayerListener then
lookAtPlayerListener:Remove()
lookAtPlayerListener = nil
end
end
end
end
end)
Player:SetParent(World)
Player.Position = {0, 40, 0}
Camera.Behavior = {
positionTarget = Player, -- camera goes to that position (or position of given object)
positionTargetOffset = { 0, 14, 0 }, -- applying offset to the target position
positionTargetBackoffDistance = 40, -- camera then tries to backoff that distance, considering collision
positionTargetMinBackoffDistance = 20,
positionTargetMaxBackoffDistance = 100,
rotationTarget = Player.Head, -- camera rotates to that rotation (or rotation of given object)
rigidity = 0.5, -- how fast the camera moves to the target
collidesWithGroups = nil, -- camera will not go through objects in these groups
}
end
-- Click to make cat jump if close enough, otherwise throws a tomato
Pointer.Click = function(pointerEvent)
-- cast ray considering only objects for impact
if cat ~= nil then
local impact = pointerEvent:CastRay(collisionGroupObjects)
if impact.Object == cat then
-- we could check impact.Distance here (distance between cat and ray origin),
-- but the distance between the player and the cat makes a bit more sense in that situation.
local diff = Player.Position - cat.Position
if diff.Length < 70 then -- check if close enough to cat
cat.Velocity.Y = 50
return
end
end
end
-- table.remove returns nil if the table is empty,
-- this is a good way to implement recycling pools.
-- (table.remove to reuse, table.insert to recycle)
local t = table.remove(tomatoes) -- get tomato from pool
if t == nil then
if tomato then
t = tomato:Copy()
end
end
if t == nil then
return -- couldn't get nor create tomato
end
t:SetParent(World)
t.Position = Player.Position + Player.Forward * 2
t.Velocity = Player.Forward * 90 + Number3(0, 100, 0)
-- recycle tomatoes after 5 seconds
Timer(5, function()
t:RemoveFromParent()
table.insert(tomatoes, t)
end)
end
-- PLAYER CONTROLS
-- jump function
Client.Action1 = function()
if Player.IsOnGround then
Player.Velocity.Y = 100
end
end
local playerSpeed = 50
-- Client.DirectionalPad is only called when x or y changes (not repeatedly)
Client.DirectionalPad = function(x, y)
Player.Motion = (Player.Forward * y + Player.Right * x) * playerSpeed
end
-- Called when Pointer is "shown" (Pointer.IsHidden == false), which is the case by default.
Pointer.Drag = function(pointerEvent)
Player.LocalRotation = Rotation(0, pointerEvent.DX * 0.01, 0) * Player.LocalRotation
Player.Head.LocalRotation = Rotation(-pointerEvent.DY * 0.01, 0, 0) * Player.Head.LocalRotation
local dpad = controls.DirectionalPadValues
Player.Motion = (Player.Forward * dpad.Y + Player.Right * dpad.X) * playerSpeed
end
Miscellaneous
Plane controls
Here's an implementation of simple plane controls.
-- constants
local ON_PRESS_ACCELERATION = 100
local ON_RELEASE_ACCELERATION = -10
local MIN_SPEED = 0
local MAX_SPEED = 300
local TILT_SPEED = 1
local ROLL_SPEED = 1
local YAW_SPEED = 1
local WING_LENGTH = 30
local DISTANCE_BETWEEN_WING_TIPS = WING_LENGTH * 2
-- variables
acceleration = 0
speed = MIN_SPEED -- default speed
dpadX = 0
dpadY = 0
-- Client.OnStart is the first function to be called when the world is launched, on each user's device.
Client.OnStart = function()
-- assuming there's an object named "plane" in the world
plane = World:FindFirst(function(o) return o.Name == "plane" end)
if plane == nil then
error("plane not found")
end
-- if not, we create one:
plane.Physics = PhysicsMode.Dynamic
plane.Position.Y = 50
World:AddChild(plane)
-- adding wing tips empty objects to calculate height difference between them
-- and use it as a simple way to affect the yaw of the plane.
-- (prefering arcade / simple physics over realistism)
leftWingTip = Object()
leftWingTip:SetParent(plane)
leftWingTip.LocalPosition:Set(-WING_LENGTH, 0, 0)
rightWingTip = Object()
rightWingTip:SetParent(plane)
rightWingTip.LocalPosition:Set(WING_LENGTH, 0, 0)
Camera.Behavior = {
positionTarget = plane,
positionTargetBackoffDistance = 100,
positionTargetMinBackoffDistance = 20,
positionTargetMaxBackoffDistance = 250,
rotationTarget = plane,
rotationTargetOffset = Rotation(math.rad(20), 0, 0),
rigidity = 0.2,
collidesWithGroups = nil,
}
end
Client.Action1 = function()
acceleration = ON_PRESS_ACCELERATION
end
Client.Action1Release = function()
acceleration = ON_RELEASE_ACCELERATION
end
Client.Tick = function(dt)
speed = math.min(MAX_SPEED, math.max(MIN_SPEED, speed + acceleration * dt))
if dpadX ~= 0 or dpadY ~= 0 then
plane.Rotation = plane.Rotation * Rotation(dpadY * dt * TILT_SPEED, 0, -dpadX * dt * ROLL_SPEED)
end
-- compare height difference between wing tips to calculate yaw
local yaw = (leftWingTip.Position.Y - rightWingTip.Position.Y) / DISTANCE_BETWEEN_WING_TIPS
local yawSpeed = yaw * dt * YAW_SPEED
plane.Rotation = Rotation(0, yaw * dt * YAW_SPEED, 0) * plane.Rotation
plane.Velocity = plane.Forward * speed
end
Client.DirectionalPad = function(x, y)
dpadX = x dpadY = y
end