Puzzle 2

The second puzzle room will combine vectors and rotations. It will mainly focus on detecting the players world rotation and look rotation.

Puzzle Overview

The second puzzle riddle is as follows:

Karl will make his move only when
suitors turn their back to him AND
keep their eye on the prize

This means the player must have their back turned to Karl and be looking at the Treasure in order for Karl to move. The Treasure will have a VFX that will only play if the player is looking at it.

Create a Server Script

In the Hierarchy, add a new script named Puzzle2Logic to the Server Context within the Scripts folder.

Add Custom Properties

The Puzzle2Logic script will need several properties to detect if the player is in the room, move the statues, and use the API script.

Expand the Escape The Room Template and find the Puzzle 2 group. This will contain the objects required by the script. Then open the Properties window with the Puzzle2Logic script selected.

Add the following custom properties:

  • From the Project Content window, drag and drop the PuzzleAPI script as a custom property.
  • Drag and drop the Statue 1 object as a custom property.
  • Drag and drop the Statue 2 object as a custom property.
  • Drag and drop the Treasure object as a custom property.
  • Drag and drop the Trigger object as a custom property.
  • Add a new custom property of type float, set the name to SolutionDistance, and value to 300.
  • Add a new custom property of type float, set the name to MoveSpeed, and value to 500.

Starting Code

The starting code is similar to the first puzzle, except there is a variable sparklesPlaying that will track if the Treasure VFX is currently playing.

Double click the Puzzle1Logic script to open the Script Editor window. Add the following code:

local API = require(script:GetCustomProperty("PuzzleAPI"))
local STATUE_1 = script:GetCustomProperty("Statue1"):WaitForObject()
local STATUE_2 = script:GetCustomProperty("Statue2"):WaitForObject()
local TREASURE = script:GetCustomProperty("Treasure"):WaitF
local TRIGGER = script:GetCustomProperty("Trigger"):WaitForObject()
local SOLUTION_DISTANCE = script:GetCustomProperty("SolutionDistance")
local MOVE_SPEED = script:GetCustomProperty("MoveSpeed")

local player = nil

local sparklesPlaying = false

local function MoveStatue(player)
   print(player.name .. " is in Puzzle Room 2.")
end

function Tick()
   if player then
      MoveStatue(player)
   end
end

local function OnBeginOverlap(trigger, other)
   if other:IsA("Player") then
      player = other
   end
end

local function OnEndOverlap(trigger, other)
   if other:IsA("Player") then
      player = nil
   end
end

TRIGGER.beginOverlapEvent:Connect(OnBeginOverlap)
TRIGGER.endOverlapEvent:Connect(OnEndOverlap)
Code language: Lua (lua)

Preview the Project

Save the script and preview the project. When the player enters the second room, the Event Log window should print a message repeatedly until the player exits.

Missing Code

The MoveStatue function will need to use the dot product of the player and Statue 1 offset vector and the player forward facing vector. It will also need to use raycasting to detect if the player’s vision is pointed at the Treasure object group.

This will be the pseudocode for the Puzzle 2 logic:

API Functions

The API script will need three new functions to be used in the second puzzle room. The first function will return the dot product of a player’s forward vector compared to the offset vector to another object. This calculated number will signify if the player is facing another object. The second function is calculating the two vectors required by a raycast of a player’s vision line. The third function is using a raycast to detect if the player is looking at an object belonging to a group of a certain name.

In the Project Content window, double click the PuzzleAPI script to open the Script Editor window. Add the following code:

local API = {}

function API.GetOffsetVector(obj1, obj2)
   local obj1Pos = obj1:GetWorldPosition()
   local obj2Pos = obj2:GetWorldPosition()
   return Vector2.New(obj2Pos) - Vector2.New(obj1Pos)
end

function API.GetDistance(obj1, obj2)
   local offset = API.GetOffsetVector(obj1, obj2)
   return offset.size
end

function API.GetTargetVelocity(obj, target, speed)
   local offset = API.GetOffsetVector(obj, target)
   local offsetNormalized = offset:GetNormalized()
   return Vector3.New(offsetNormalized * speed, 0)
end

function API.GetFacingDotProduct(obj1, obj2)
   local obj1Forward = obj1:GetWorldRotation() * Vector3.FORWARD
   local offset = API.GetOffsetVector(obj1, obj2)
   local offsetNormalized = Vector3.New(offset:GetNormalized(), 0)
   return obj1Forward .. offsetNormalized
end

function API.GetPlayerLookAtVectors(player, length)
   local playerPos = player:GetWorldPosition()
   local headOffset = Vector3.New(0, 0, 100)
   local headPos = playerPos + headOffset
   local playerLookRotation = player:GetLookWorldRotation()
   local playerLookForward = playerLookRotation * Vector3.FORWARD * length
   return headPos, headPos + playerLookForward
end

function API.LookingAtGroup(player, group)
   local v1, v2 = API.GetPlayerLookAtVectors(player, 3000)
   local hitResult = World.Raycast(v1, v2, {ignorePlayers = true})

   if hitResult and hitResult.other and group:IsAncestorOf(hitResult.other) then
      return true
   end

   return false
end

return API
Code language: Lua (lua)

Raycasting

The World.Raycast function will return a HitResult object if the raycast collides with any object going from the starting vector position to the ending vector position. This is useful for checking a player’s vision line or checking a projectile collision.

Display the Treasure VFX

Using the raycasting function from the API will detect if the player is looking at the treasure. The effect should be played if it is not currently playing and the player is looking at the treasure. The effect should be stopped if the effect is playing and the player is not looking at the treasure.

Open the Puzzle2Logic script. Add this code to the MoveStatue function:

local function MoveStatue(player)
   local lookingAtTreasure = API.LookingAtGroup(player, TREASURE)

   if not sparklesPlaying and lookingAtTreasure then
      Events.BroadcastToPlayer(player, "PlaySparkle")
      sparklesPlaying = true
   end

   if sparklesPlaying and not lookingAtTreasure then
      Events.BroadcastToPlayer(player, "StopSparkle")
      sparklesPlaying = false
   end
endCode language: Lua (lua)

Preview the Project

Save the script and preview the project. Try toggling the special effect by looking at the treasure and then away.

Display the Dot Product

The dot product of the player offset vector to Statue 1 and its forward facing vector will return a number between -1 and 1. If the number is positive, then the player is facing in the general direction of the statue. If it is negative, then the player is facing away.

Open the Puzzle2Logic script. Add this code to the MoveStatue function:

local function MoveStatue(player)
   local lookingAtTreasure = API.LookingAtGroup(player, TREASURE)

   if not sparklesPlaying and lookingAtTreasure then
      Events.BroadcastToPlayer(player, "PlaySparkle")
      sparklesPlaying = true
   end

   if sparklesPlaying and not lookingAtTreasure then
      Events.BroadcastToPlayer(player, "StopSparkle")
      sparklesPlaying = false
   end

   local dotProduct = API.GetFacingDotProduct(player, STATUE_1)
   local isFacingAwayFromStatue = dotProduct < 0
   print("Player facing away from statue is " .. tostring(isFacingAwayFromStatue))
   print("The dot product is " .. tostring(dotProduct))
end
Code language: Lua (lua)

Preview the Project

Save the script and preview the project. Check the Event Log window to see if the player’s direction is correctly recognizing whether it is facing the statue.

Finish the Code

Now that both conditions are being checked, it is time to put it all together to control the Statue 1 movement and puzzle door opening.

Open the Puzzle2Logic script. Add this code to the MoveStatue function:

local function MoveStatue(player)
   local lookingAtTreasure = API.LookingAtGroup(player, TREASURE)

   if not sparklesPlaying and lookingAtTreasure then
      Events.BroadcastToPlayer(player, "PlaySparkle")
      sparklesPlaying = true
   end

   if sparklesPlaying and not lookingAtTreasure then
      Events.BroadcastToPlayer(player, "StopSparkle")
      sparklesPlaying = false
   end

   local dotProduct = API.GetFacingDotProduct(player, STATUE_1)
   local isFacingAwayFromStatue = dotProduct < 0
   local targetDistance = API.GetDistance(STATUE_1, STATUE_2)
   local velocity = API.GetTargetVelocity(STATUE_1, STATUE_2, MOVE_SPEED)

   if targetDistance <= SOLUTION_DISTANCE then
      STATUE_1:StopMove()
      Events.Broadcast("OpenPuzzleDoor", "2")
   elseif isFacingAwayFromStatue and lookingAtTreasure then
      STATUE_1:MoveContinuous(velocity)
   else
      STATUE_1:StopMove()
   end
end
Code language: Lua (lua)

Preview the Project

The second puzzle should now be working!

Post a comment

Leave a Comment

Scroll to Top