Here is a very simple script that shows a message to a user when they touch an object:
--lua
function onUserTouchedObject(av : Avatar, ob : Object)
showMessageToUser("Hi " .. av.name .. ", you touched the red cube!", av)
end
Which has this result in-world when you walk into the red cube:
When a user touches an object, it shoots the user into the air
--lua function onUserTouchedObject(avatar : Avatar, ob : WorldObject) local v = avatar.linear_velocity local new_v = Vec3f(v.x, v.y, 10.0) avatar.linear_velocity = new_v
This script should be applied to a text object. It changes the text to read 'User entered parcel' when the user enters the parcels the object is in, and to 'User exited parcel' when they leave the parcel. It also changes the text colour.
--lua function onUserEnteredParcel(user : User, ob : WorldObject, parcel : Parcel) ob.content = "User entered parcel" mat = ob:getMaterial(0) mat.colour = Vec3f(0,1,0) end function onUserExitedParcel(user : User, ob : WorldObject, parcel : Parcel) ob.content = "User exited parcel" mat = ob:getMaterial(0) mat.colour = Vec3f(1,0.1,0) end
--lua function onUserEnteredParcel(user : User, ob : WorldObject, parcel : Parcel) mat = ob:getMaterial(0) mat.colour = Vec3f(1,0,0) end function onUserExitedParcel(user : User, ob : WorldObject, parcel : Parcel) mat = ob:getMaterial(0) mat.colour = Vec3f(0,1,0) end
--lua
local near_user = nil
function onTimerEvent(ob : WorldObject)
if(near_user) then
local dx = near_user.pos.x - ob.pos.x
local dy = near_user.pos.y - ob.pos.y
local dz = (near_user.pos.z - 1.68) - ob.pos.z
d = math.sqrt(dx*dx + dy*dy + dz*dz)
if(d > 1.0) then
dx /= d
dy /= d
dz /= d
step_dist = 0.4
local newx = ob.pos.x + dx * step_dist
local newy = ob.pos.y + dy * step_dist
local newz = ob.pos.z + dz * step_dist
ob.pos = {x=newx, y=newy, z=newz}
end
end
end
createTimer(onTimerEvent, 0.1, true)
function onUserMovedNearToObject(user : User, ob : WorldObject)
print("onUserMovedNearToObject!!!")
near_user = user
end
function onUserMovedAwayFromObject(user : User, ob : WorldObject)
print("onUserMovedAwayFromObject!!!")
near_user = nil
end
This script shows how to fetch information from somewhere else on the web, using the doHTTPPostRequestAsync function. We will use it to get the current Ethereum gas price from Infura, and update a text object in substrata to show the current gas price.
This example uses parseJSON to convert JSON returned from the HTTP request to a Lua object, and getSecret to get infura_project_id, which is a private API key.
--lua
function onDone(res)
print("response_code: " .. tostring(res.response_code))
print("response_message: " .. res.response_message)
print("mime_type: " .. res.mime_type)
print("body_data: " .. string.sub(buffer.tostring(res.body_data), 1, 100))
local data = parseJSON(buffer.tostring(res.body_data))
local price_num = tonumber(data.result)
this_object.content = "Ethereum gas price: " .. string.format("%.2f", price_num * 1.0e-9) ..
" Gwei"
end
function onError(res)
print("error_code: " .. tostring(res.error_code))
print("error_description: " .. res.error_description)
end
infura_project_id = getSecret('infura_project_id')
print("infura_project_id " .. tostring(infura_project_id))
function onTimerEvent()
print("Fetching current gas price...")
-- See https://docs.infura.io/api/networks/ethereum/json-rpc-methods/eth_gasprice
doHTTPPostRequestAsync("https://mainnet.infura.io/v3/" .. infura_project_id,
'{"jsonrpc":"2.0","method":"eth_gasPrice","params": [],"id":1}', -- post content
"application/json", -- content type
{}, -- additional header lines
onDone, onError)
end
if(IS_SERVER) then
createTimer(onTimerEvent, 60.0, true) -- Call onTimerEvent every 60 seconds, repeatedly.
else
print("running on client, not doing anything.")
end
This script shows how to fetch information from somewhere else on the web, using the doHTTPGetRequestAsync function. We will use it to get the current Eth/USD exchange rate from Coinbase, and update a text object in Substrata with it.
--lua
function onDone(res)
local json_data = parseJSON(buffer.tostring(res.body_data))
local price_num = tonumber(json_data.data.rates.USD)
this_object.content = "Eth price: " .. string.format("%d", price_num) .. " USD"
end
function onError(res)
print("error_code: " .. tostring(res.error_code))
end
function onTimerEvent()
print("Fetching current ETH/USD rate...")
-- See https://docs.cdp.coinbase.com/coinbase-app/docs/api-exchange-rates
doHTTPGetRequestAsync("https://api.coinbase.com/v2/exchange-rates?currency=ETH",
{}, -- additional header lines
onDone, onError)
end
if(IS_SERVER) then
createTimer(onTimerEvent, 60.0, true)
end
This is a complete race script for bicycle races, with a leaderboard.
To set it up, apply the script to an object. You will also need to create some waypoint sensor objects, then get their UIDs and edit the script to use the waypoint ids on the 'waypoint_uids' line.
scoreboard_text_ob_uid should be the UID of a text object which will show the high score list
This script uses objectstorage.setItem and objectstorage.getItem to store the high-score data persistently.
--lua
local race_info = {} -- A map from avatar UID to per-user race info.
local waypoint_uids = {587, 583, 584, 585, 587} -- UIDs of waypoint sensor objects. The first object is the start line sensor, the last object is the finish line sensor.
local reset_ob_uid = 588 -- Touching this object will reset the race state.
local scoreboard_text_ob_uid = 589
local MAX_NUM_BEST_TIMES = 5
local best_times = nil -- An array of the top MAX_NUM_BEST_TIMES best times. Each entry is a table { av_uid : UID, av_name : string, time : number }
best_times = objectstorage.getItem("best_times")
--print("Result of loading best_times from object storage: " .. tostring(best_times))
if best_times ~= nil then
print("loaded " .. tostring(#best_times) .. " best times from object storage!")
else
print("No best times found in object storage, starting with empty best times.")
best_times = {}
end
print("best_times: ")
print(best_times)
print(tostring(best_times))
print("race_info: ")
print(race_info)
print(tostring(race_info))
function updateScoreboardText()
local s = "---Best times---\n"
for i = 1, #best_times do
s = s .. tostring(i) .. ": " .. best_times[i].av_name .. ": " .. string.format("%.3f", best_times[i].time) .. " s\n"
end
getObjectForUID(scoreboard_text_ob_uid).content = s
end
function checkAddTimeToBestTimes(av : Avater, time_s : number)
for i = 1 , #best_times do
if(time_s < best_times[i].time) then -- If the new time was faster than time i:
-- Move time i and other times to the right one place to the right
-- Do this by starting at the rightmost index we want to move the time to, copy from the index to the left, and then iterate left.
for z = math.min(#best_times + 1, MAX_NUM_BEST_TIMES), i+1, -1 do
best_times[z] = best_times[z-1]
end
-- Set time i
best_times[i] = { av_uid = av.uid, av_name = av.name, time = time_s }
-- Save updated best times to object storage
objectstorage.setItem("best_times", best_times)
updateScoreboardText()
return
end
end
-- If we got here the new time was not better than any of the existing times.
-- If not all slots were occupied on scoreboard:
if(#best_times < MAX_NUM_BEST_TIMES) then
best_times[#best_times + 1] = { av_uid = av.uid, av_name = av.name, time = time_s } -- Add new time
-- Save updated best times to object storage
objectstorage.setItem("best_times", best_times)
updateScoreboardText()
end
end
function onUserTouchedObject(av : Avater, ob : Object)
print("User touched object " .. tostring(ob.uid))
-- Get or create per-user race information (user_race_info)
local user_race_info = race_info[av.uid]
if(not user_race_info) then
print("Making new user_race_info...")
user_race_info = {next_waypoint_index=1, valid=true}
race_info[av.uid] = user_race_info
end
print("user_race_info.next_waypoint_index: " .. tostring(user_race_info.next_waypoint_index) .. ", valid: " .. tostring(user_race_info.valid))
if(ob.uid == reset_ob_uid) then
if((user_race_info.next_waypoint_index == 1) or not user_race_info.valid) then -- If user has not started race, or their race was invalid (went through wrong gates)
-- Prepare to start/restart the race
showMessageToUser("prepare to start race, move through start gate to start!", av)
user_race_info.next_waypoint_index = 1
user_race_info.valid = true
end
return
end
if(not user_race_info.valid) then
showMessageToUser("Touch the restart object to restart the race.", av)
return
end
if((user_race_info.next_waypoint_index >= 2) and (ob.uid == waypoint_uids[user_race_info.next_waypoint_index-1])) then
-- User went through the same waypoint again
showMessageToUser("You need to go through the next waypoint!", av)
elseif(ob.uid == waypoint_uids[user_race_info.next_waypoint_index]) then -- If user went through correct waypoint:
if(user_race_info.next_waypoint_index == 1) then -- If user went through the start gate:
local vehicle = av.vehicle_inside
if(not vehicle) then
showMessageToUser("You must be in a vehicle!", av)
user_race_info.valid = false
return
end
if((vehicle.mass < 199.99) or (vehicle.mass > 200.01)) then
showMessageToUser("You must be in a standard bike!", av)
user_race_info.valid = false
return
end
showMessageToUser("Race Started!", av)
user_race_info.vehicle = vehicle
-- Add event listener for the user exiting the vehicle, so we can disqualify them if they are racing.
addEventListener("onUserExitedVehicle", vehicle.uid, onUserExitedVehicle)
user_race_info.start_time = getCurrentTime()
user_race_info.next_waypoint_index = user_race_info.next_waypoint_index + 1
elseif(user_race_info.next_waypoint_index == #waypoint_uids) then -- If user went through the last gate (finish line):
-- race completed!
local user_time = getCurrentTime() - user_race_info.start_time
showMessageToUser("Finish! your time: " .. string.format("%.3f", user_time) .. " s", av)
checkAddTimeToBestTimes(av, user_time)
--if(user_time < best_time) then
-- print("You set the best time..")
-- best_time = user_time
-- getObjectForUID(scoreboard_text_ob_uid).content = "Best time: " .. string.format("%.3f", user_time) .. " s by " .. av.name
--end
--user_race_info.next_waypoint_index = 1 -- restart
-- Now that the race is completed, just mark it as invalid, so we won't get messages when exiting vehicle etc.
user_race_info.valid = false
else
showMessageToUser("waypoint " .. tostring(user_race_info.next_waypoint_index - 1) .. "/" .. tostring(#waypoint_uids - 2) .. " done", av)
user_race_info.next_waypoint_index = user_race_info.next_waypoint_index + 1
end
else -- else if user went through an incorrect gate (wrong order):
showMessageToUser("You went through the wrong waypoint!", av)
user_race_info.valid = false
end
end
function onUserExitedVehicle(av : Avatar, vehicle : Object)
print("onUserExitedVehicle")
local user_race_info = race_info[av.uid]
if(user_race_info and user_race_info.valid) then
showMessageToUser("You exited your vehicle during the race and were disqualified!", av)
user_race_info.valid = false
end
end
-- Add event listeners for the onUserTouchedObject event on our waypoint sensor objects
for key, uid in waypoint_uids do
addEventListener("onUserTouchedObject", uid, onUserTouchedObject)
end
addEventListener("onUserTouchedObject", reset_ob_uid, onUserTouchedObject)