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