i’m switching to Knit! I’m sorry for everyone who learnt AGF for this post :((
next thing: making the bars in the enemies’ position, and making them the smaller the more distant they are
THIS IS A WORK IN PROGRESS! POST LAST EDITED: 24/04/2021 PROJECT LAST EDITED: 11/05/2021
Henlo, fellow humans! Zamdie here, and I’ll guide you through this tutorial! ☆*:.。.o(≧▽≦)o.。.:*☆
We’ll be creating a stealth system, and for that, I’ve compiled everything you need in order to fry the pink jelly inside your head!
Let’s start, shall we? \(^▽^)/
0.1: Things our system will contain:
- Enemy detection (enemy will detect the player);
- Enemy paths (enemies will patrol areas);
- Enemy takedown (players will be able to neutralize/kill enemies);
- Enemy reactions (enemies will try to kill the player once they’re detected);
0.2: Stuff you need to know before trying to understand this tutorial
I won’t be explaining these things in the tutorial
- OOP basics; Useful article
- Functions; Useful article
- Modules; Useful article
- Basic math operations(+,-,*,/);
- Services, and their events, arguments, uses, etc. should come naturally, but if you don’t have too much experience, you can always use the API reference;
- General objects (events, properties, uses, etc.) again, if you don’t have experience, search the API reference ;
- Type checking (optional, but recommended); Perhaps my tutorial’ll be useful
1: Before we start
I’m going to use AeroGameFramework because it makes everything easier (─‿‿─). You don’t have to use it, but I’ll be explaining stuff on it (don’t worry, I’ll include sections to explain what to do if you aren’t using AGF (ノ_<。)ヾ(^▽^) ). I’ll be using Visual Studio Code with Rojo aswell, with the Roblox LSP extension which is god tier IMO ଘ(੭ˊ꒳ˋ)੭✧ . advanced intellisense, something Roblox’s editor lacks. One GetService
or WaitForChild
and Studio refuses to give you intellisense ヾ(`ヘ´)ノ゙
2: Setting up
I’m gonna make it OOP. Why, you ask ? Because we can have one controller script and a module to handle all the logic. (´• ω •`)
So, let’s create one controller called StealthController
in Client > Controllers
:
and a class module called Enemy
under Shared
:
Then, create a ScreenGui
named DetectionUIS
in StaterGui
:
and design a detection bar. Put it in ReplicatedStorage
. I’ve set it up like this:
However, depending on the location of the actual bar (the part that you want to go up and down) you might want to change the code a bit.
Without AGF
Create a ModuleScript
under ReplicatedStorage
named Enemy
:
and a LocalScript
under StarterPlayer > StarterPlayerScripts
named StealthController
:
First, let’s set-up our Enemy
class.
-- Enemy
-- zamd157
-- December 19, 2020
-- Last edited: April 24, 2021
-- Basic OOP setup
local Enemy = {}
Enemy.__index = Enemy
-- Services
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local PathfindingService = game:GetService("PathfindingService")
local TweenService = game:GetService("TweenService")
-- Objects
local detectionBar = ReplicatedStorage:WaitForChild("detectionBar")
-- Constants
local RAYCAST_PARAMS: RaycastParams = RaycastParams.new()
RAYCAST_PARAMS.FilterType = Enum.RaycastFilterType.Blacklist
local POSITION_TWEEN_INFO: TweenInfo = TweenInfo.new(
0.3,
Enum.EasingStyle.Quint,
Enum.EasingDirection.In
)
-- Modules
local Signal
-- Module initialization
function Enemy:Init()
-- Assign Signal
Signal = self.Shared.Signal
end
return Enemy
Without AGF
Copy the source of the Signal
module from Nevermore engine’s GitHub, paste it in a ModuleScript
inside ReplicatedStorage
, and name it Signal
.
In the Enemy
class, remove the :Init()
method, and require the Signal
module on the variable declaration:
-- With AGF
local Signal -- Will be assigned under the AGF :Init() method
-- Without AGF
local Signal = require(game:GetService("ReplicatedStorage"):WaitForChild("Signal"))
Let’s make our class constructor now!
-- Constructor
function Enemy.new(enemyModel: Model)
-- References to stuff inside the enemy model
local animator = Instance.new("Animator"); animator.Parent = enemyModel.Humanoid;
--local animationsFolder = enemyModel.animations
-- Grab the local player, it'll be nil in methods called from server
local plr: Player? = (RunService:IsClient()) and game:GetService("Players").LocalPlayer or nil
-- Create our object!
local self = setmetatable({
-- Properties
-- Reference to the enemies' model
enemy = enemyModel,
-- Events
OnDetected = Signal.new(),
-- Variables to store info
detected = false,
detecting = false,
distance = 0,
detectionMeter = 0,
-- Player, this'll be nil in methods to be called from server
player = plr,
-- The frame that is going to be the detection bar, it'll be nil if the local player is
detectionBar = (plr ~= nil) and detectionBar:Clone() or nil,
combatSettings = {
damage = 25,
attackCooldown = 3,
range = 20,
},
--[[animations = {
fire = animator:LoadAnimation(animationsFolder.fire),
reload = animator:LoadAnimation(animationsFolder.reload),
walk = animator:LoadAnimation(animationsFolder.walk),
},]]
detectionSettings = {
FOV = 0.5,
maxDistance = 50,
minDetection = 0,
maxDetection = 100,
DISTANCE_CONSTANT = 20
}
}, Enemy)
-- Parent the detection bar to the detection ui's ScreenGui if it isn't nil
if (self.detectionBar) then
plr:WaitForChild("PlayerGui")
plr.PlayerGui:WaitForChild("DetectionUIS")
self.detectionBar.Parent = plr.PlayerGui.DetectionUIS
end
return self
end
Cool! Let’s set-up the controller now:
-- Stealth Controller
-- zamd157
-- December 19, 2020
-- Last edited: April 24, 2021
-- Controller
local StealthController = {}
-- Roblox Services
local RunService = game:GetService("RunService")
-- Modules
local Enemy
-- Table to store our enemies
local enemies = {}
-- The folder with the models of our enemies
local enemyFolder = workspace:WaitForChild("enemies"):GetChildren()
-- Types
-- this is poggers men!
type Enemy = {
-- Properties
enemy: Model,
OnDetected: RBXScriptSignal,
detected: boolean,
detecting: boolean,
distance: number,
detectionMeter: number,
player: Player?,
combatSettings: {
damage: number,
attackCooldown: number,
range: number
},
animations: {
fire: AnimationTrack,
reload: AnimationTrack,
walk: AnimationTrack
},
detectionSettings: {
FOV: number,
maxDistance: number,
minDetection: number,
maxDetection: number,
DISTANCE_CONSTANT: number
},
-- Objects
detectionBar: Frame?,
-- Methods
new: (Model) -> Enemy,
DoChecks: () -> nil,
MoveToNextPoint: () -> nil,
-- Private methods
_behindObstacle: () -> boolean,
_withinFOV: () -> boolean,
_withinDistance: () -> boolean,
_updateGUI: () -> nil,
Init: () -> nil
}
-- Controller initialization
function StealthController:Init()
-- Assign enemy
Enemy = self.Shared.Enemy
end
return StealthController
Without AGF
Do as before, assign the ModuleScript
named Enemy
under ReplicatedStorage
:
-- With AGF
local Enemy -- Will be assigned under the AGF :Init() method
-- Without AGF
local Enemy = require(game:GetService("ReplicatedStorage"):WaitForChild("Enemy"))
Great! Now that we have the Enemy
class set-up, let’s code our core controller logic:
function StealthController:Start()
-- Loop through the enemies's models
for enemyIndex: number, enemy: Model in ipairs(enemyFolder) do
-- Create an instance of the Enemy class
local newEnemy = Enemy.new(enemy)
-- Create a key for our new Enemy object
enemies[enemy.Name .. enemyIndex] = newEnemy
-- Connect the .OnDetected event
local onDetected: RBXScriptConnection
onDetected = newEnemy.OnDetected:Connect(function()
-- Here is where you're going to get creative
-- You could make this assault-based like Payday 2,
-- or perhaps a "i-saw-you-so-i-attack-you" system
-- like Thief, Assassin's Creed, Dishonored, etc.
-- I'm going with the second option
-- We'll code this later, for now, a print statement does the job
print("Detected!")
onDetected:Disconnect()
end)
end
end
Without AGF
This is a bit trickier to convert to non-AGF scripts.
Think of the AGF methods like this:
- :Init() - Variable declarations for things inside the framework;
- :Start() - Normal body of the script;
So as the Enemy
module’s :Init()
method, to convert the variable declaration, simply put it at the top of the script:
-- With AGF
local Enemy -- Will be assigned under the AGF :Init() method
-- Without AGF
-- Simply assign it on the variable declaration
local Enemy = require(game:GetService("ReplicatedStorage"):WaitForChild("Enemy"))
For the :Start()
method, simply put everything in it on the top scope of the script, remove the method, the table and the return statement:
-- Loop through the enemies's models
for enemyIndex: number, enemy: Model in ipairs(enemyFolder) do
-- Create an instance of the Enemy class
local newEnemy = Enemy.new(enemy)
-- Create a key for our new Enemy object
enemies[enemy.Name .. enemyIndex] = newEnemy
-- Connect the .OnDetected event
local onDetected: RBXScriptConnection
onDetected = newEnemy.OnDetected:Connect(function()
-- Here is where you're going to get creative
-- You could make this assault-based like Payday 2,
-- or perhaps a "i-saw-you-so-i-attack-you" system
-- like Thief, Assassin's Creed, Dishonored, etc.
-- I'm going with the second option
-- We'll code this later, for now, a print statement does the job
print("Detected!")
onDetected:Disconnect()
end)
end
Full non-AGF controller (for now)
-- Stealth Controller
-- zamd157
-- December 19, 2020
-- Last edited: April 24, 2021
-- Controller
local StealthController = {}
-- Roblox Services
local RunService = game:GetService("RunService")
-- Modules
local Enemy = require(game:GetService("ReplicatedStorage"):WaitForChild("Enemy"))
-- Table to store our enemies
local enemies = {}
-- The folder with the models of our enemies
local enemyFolder = workspace:WaitForChild("enemies"):GetChildren()
-- Types
-- this is poggers men!
type Enemy = {
-- Properties
enemy: Model,
OnDetected: RBXScriptSignal,
detected: boolean,
detecting: boolean,
distance: number,
detectionMeter: number,
player: Player?,
combatSettings: {
damage: number,
attackCooldown: number,
range: number
},
animations: {
fire: AnimationTrack,
reload: AnimationTrack,
walk: AnimationTrack
},
detectionSettings: {
FOV: number,
maxDistance: number,
minDetection: number,
maxDetection: number,
DISTANCE_CONSTANT: number
},
-- Objects
detectionBar: Frame?,
-- Methods
new: (Model) -> Enemy,
DoChecks: () -> nil,
MoveToNextPoint: () -> nil,
-- Private methods
_behindObstacle: () -> boolean,
_withinFOV: () -> boolean,
_withinDistance: () -> boolean,
_updateGUI: () -> nil,
Init: () -> nil
}
-- Loop through the enemies's models
for enemyIndex: number, enemy: Model in ipairs(enemyFolder) do
-- Create an instance of the Enemy class
local newEnemy = Enemy.new(enemy)
-- Create a key for our new Enemy object
enemies[enemy.Name .. enemyIndex] = newEnemy
-- Connect the .OnDetected event
local onDetected: RBXScriptConnection
onDetected = newEnemy.OnDetected:Connect(function()
print("Detected!")
onDetected:Disconnect()
end)
end
3: Detection System
Alright, first, let’s plan out what we’re going to do.
- Check if player is within detection distance;
- Check if player is within enemy’s FOV;
- Check if player is behind something;
If these conditions are met, the enemy is going to raise their detection up, depending on the detection increment which is based on the distance from the player.
So, let’s get to the good stuff
First, create a method called DoChecks()
inside your Enemy
module:
-- Do our checks
function Enemy:DoChecks()
end
Alright! Let’s make some logic functions now!
Starting with our less expensive check, we’ll use the player:DistanceFromCharacter() method, then compare it with our maxDistance
setting:
function Enemy:_withinDistance()
-- If player's character doesn't exist, return false
if (not self.player.Character) then return false end
-- Grab le distance
self.distance = self.player:DistanceFromCharacter(self.enemy.Head.Position)
-- If distance is smaller than the max distance, return true, else, false
return (self.distance <= self.detectionSettings.maxDistance)
end
Aight’, with our second check, we’ll use dot product to determine if our player is within FOV of the enemy. Check out this awesome tutorial by okeanskiy REMEMBER TO PUT @ BEFORE IT OK ZAMDIE? about it, this topic also explains it very well. Check them out for explanation! because my brain can’t process that information… yet
function Enemy:_withinFOV()
-- If player's character doesn't exist, return false
if (not self.player.Character) then return false end
-- Get le unit of the player's head to the enemy's head
local npcToCharacter = (
self.player.Character.Head.Position - self.enemy.Head.Position
).Unit
-- Get le LookVector of the head of the enemy's CFrame
local npcLook = self.enemy.Head.CFrame.LookVector
-- Grab le dot product
local dotProduct = npcToCharacter:Dot(npcLook)
-- If the dot product is bigger than the FOV, returns true, else, false
return (dotProduct > self.detectionSettings.FOV)
end
For our last check which will make weak computing devices die , we’ll raycast from the enemy’s head to the player’s head, to check if there is something between them.
function Enemy:_behindObstacle()
-- If player's character doesn't exist, return false
if (not self.player.Character) then return false end
-- Raycast!!!!!!!11
-- Please note that if we fire the ray from the head to the head,
-- it will be only detected if the head is exposed. So if the
-- torso and legs are showing, but the head isn't, the player's
-- not going to get detected.
local raycastResult = workspace:Raycast(
self.enemy.Head.Position, -- Origin
(self.player.Character.Head.Position - self.enemy.Head.Position), -- Direction
RAYCAST_PARAMS -- Raycast params
)
-- If ray hits, we do stuff lol
if (raycastResult) then
-- This checks if the parent is a model(character).
-- If it isn't, it's the handle of an accessory, so it sets to the parent of the parent
local char = (raycastResult.Instance.Parent:IsA("Model"))
and raycastResult.Instance.Parent -- If it's a limb
or raycastResult.Instance.Parent.Parent -- If it's a Handle of an accessory
-- If it is actually a character
if (char:FindFirstChild("Humanoid")) then
-- Returns true if the character hit is of our player's (because it can hit other players or enemies), else, returns false
return (char.Name == self.player.Name)
end
end
-- Returns false if ray didn't hit anything, or didn't hit a character
return false
end
Phew! That was quite a bit of code, oh boy did I not know what was coming… let’s put our functions in our method and do magic!
-- Do our checks
function Enemy:DoChecks()
-- Fire our functions, if they are detected, set detecting to true
if (self:_withinDistance()) then
if (self:_withinFOV()) then
if (self:_behindObstacle()) then
self.detecting = true
end
end
end
-- Call the function to update the detection bar
self:_updateGUI()
-- Set detecting to false again
self.detecting = false
end
Oh, so you mere mortal thought we were done with the hard part? Nope, we’re just starting.
You saw I called a method called _updateGUI()
. Yes, you definitely saw. We’re doing the detection bar next.
4: Coding the detection bar
Ladies and gentlemen, fasten your seatbelts brains because we’re going to experience a turbulence.
Really.
I’ve explained everything in comments, good luck, my friend!
(I’m too lazy to explain everything now, here above the script, I’ll explain it in detail when I release it as a tutorial!)
function Enemy:_updateGUI()
-- If detecting, do all the stuff below
if (self.detecting) then
-- Here we add the current detection to a constant divided by the distance
-- I saw this on the devforum, however I can't find the topic again
-- Basically, saying our constant is 20, it works like this:
-- 20 / 5 (5 is very close) = 4
-- 20 / 50 (50 is very far) = 0,4
-- So, the meter is gonna go up slower when the value is low, and the bigger, the faster
self.detectionMeter = math.clamp(
(self.detectionMeter +
(self.detectionSettings.DISTANCE_CONSTANT / self.distance)
),
self.detectionSettings.minDetection,
self.detectionSettings.maxDetection
)
-- If the bar isn't showing up, make it show
if (not self.detectionBar.Visible) then
self.detectionBar.Visible = true
end
-- If the detection is the max detection, fire the OnDetected event and set detected to true
if (self.detectionMeter == self.detectionSettings.maxDetection) then
self.OnDetected:Fire()
self.detected = true
self.detectionBar.Visible = false
else
-- Else, we resize the detection bar to go up
-- Btw, Udim2.fromScale is Udim2.new but without the offset values
-- Basically, the code below is the same as Udim2.new(1, 0, mathstuff, 0)
self.detectionBar.bar.Size = UDim2.fromScale(
1,
-(self.detectionMeter / self.detectionSettings.maxDetection)
)
-- TODO; POSITION THE GUI IN THE DIRECTION OF THE ENEMY
end
else
-- If not detecting, do the stuff below
-- If the bar is not visible, return (as we won't need to do more stuff)
if (not self.detectionBar.Visible) then return end
-- If the bar isn't the smallest size, we make it go down, else, it's completely down
-- and we make it not visible
if (self.detectionBar.bar.Size.Y.Scale ~= 0) then
-- This math is the same we do when detecting,
-- except we subtract it and divide the distance by the distance constant
-- (before we added it, and divided the distance constant by the distance)
-- This'll make so the closer you are, the slower the bar goes down
self.detectionMeter = math.clamp(
(self.detectionMeter -
(self.distance / self.detectionSettings.DISTANCE_CONSTANT)
),
self.detectionSettings.minDetection,
self.detectionSettings.maxDetection
)
-- The resizing is the same
self.detectionBar.bar.Size = UDim2.fromScale(
1,
-(self.detectionMeter / self.detectionSettings.maxDetection)
)
else
-- I spent like 2 hours figuring why this else wasn't running
-- It was because I forgot to add .bar and it was checking the
-- Size of the detectionBar (the background of the bar)
-- Make the detection bar not visible anymore
self.detectionBar.Visible = false
end
end
end
Hey, you did it! You did a nice job!
One more small step, slap a nice loop in our StealthController
to call the :DoChecks()
method!
function StealthController:Start()
-- Loop through the enemies's models
for enemyIndex: number, enemy: Model in ipairs(enemyFolder) do
-- Create an instance of the Enemy class
local newEnemy = Enemy.new(enemy)
-- Create a key for our new Enemy object
enemies[enemy.Name .. enemyIndex] = newEnemy
-- Connect the .OnDetected event
local onDetected: RBXScriptConnection
onDetected = newEnemy.OnDetected:Connect(function()
print("Detected!")
onDetected:Disconnect()
end)
end
-- Function to be run every heartbeat
local function heartBeat(_: number)
-- Loops through our enemies
for _: string, enemy: Enemy in pairs(enemies) do
-- If enemy has been detected, skip the iteration to not call the method
if (enemy.detected) then continue end
enemy:DoChecks()
end
end
RunService.Heartbeat:Connect(heartBeat)
end
Take a small break and appreciate what we’ve done so far:
I swear the rest of the tutorial is going to be easier!
5: Guard routes - pathfinding
First things first, create a service named EnemyService
under Server
:
Now make a folder called waypoints
in Workspace
, and then folders with the names of the enemies and the number.
Now that’s done, let’s code the plate we’re going to work with:
-- Enemy Service
-- zamd157
-- December 19, 2020
-- Last edited: April 24, 2021
-- Service
local EnemyService = {Client = {}}
-- Roblox services
local RunService = game:GetService("RunService")
-- Modules
local Enemy
-- Table to store our enemies
local enemies = {}
-- The folder with the models of our enemies
local enemyFolder = workspace:WaitForChild("enemies"):GetChildren()
-- Folder with the enemies's waypoints
local waypoints = workspace:WaitForChild("waypoints")
-- Types
type Enemy = {
-- Properties
enemy: Model,
OnDetected: RBXScriptSignal,
detected: boolean,
detecting: boolean,
distance: number,
detectionMeter: number,
player: Player?,
combatSettings: {
damage: number,
attackCooldown: number,
range: number
},
animations: {
fire: AnimationTrack,
reload: AnimationTrack,
walk: AnimationTrack
},
detectionSettings: {
FOV: number,
maxDistance: number,
minDetection: number,
maxDetection: number,
DISTANCE_CONSTANT: number
},
-- Objects
detectionBar: Frame?,
-- Methods
new: (Model) -> Enemy,
DoChecks: (Enemy) -> nil,
MoveToNextPoint: (Enemy) -> nil,
-- Private methods
_behindObstacle: (Enemy) -> boolean,
_withinFOV: (Enemy) -> boolean,
_withinDistance: (Enemy) -> boolean,
_updateGUI: (Enemy) -> nil,
Init: (Enemy) -> nil
}
type Array<T> = {[number] : T}
-- Local util functions
-- You honestly don't have to use this Heartbeat based wait,
-- I'm just using it because why not? 0.1 second accuracy is nice!
local function wait(seconds: number)
seconds = seconds or (1/60)
local deltaTime = 0
while (deltaTime < seconds) do
deltaTime = deltaTime + RunService.Heartbeat:Wait()
end
return deltaTime
end
function EnemyService:Init()
Enemy = self.Shared.Enemy
end
return EnemyService
Next, let’s make a new method in the Enemy
class called :MoveToNextPoint()
. In it, we’re going to handle pathfinding:
-- Enemy route pathfinding
function Enemy:MoveToNextPoint()
local path = self.path
-- Part to go to
local waypointPart = path.waypoints[path.waypoint]
-- Event when the route is blocked
local routeBlocked: RBXScriptConnection
-- Tween to have a nice transition when the character stays in the waypoint part
local positionTween = TweenService:Create(
self.enemy.PrimaryPart,
POSITION_TWEEN_INFO,
{
-- In the part's position, looking at the direction the part is looking
CFrame = CFrame.new(
waypointPart.Position,
waypointPart.CFrame.LookVector
)
}
)
-- Create the path
path.route = PathfindingService:CreatePath()
-- Compute the path
path.route:ComputeAsync(
self.enemy.PrimaryPart.Position, -- Start position
waypointPart.Position -- End position
)
-- If the path status isn't success, we have to make 'em stop
if (path.route.Status ~= Enum.PathStatus.Success) then
self.enemy.Humanoid:MoveTo(self.enemy.PrimaryPart.Position)
end
-- Connect the Blocked event, so it calls itself again aka. recursive
routeBlocked = path.route.Blocked:Connect(function()
routeBlocked:Disconnect()
self:MoveToNextPoint()
end)
-- Loop through the waypoints
for _, waypoint: PathWaypoint in pairs(path.route:GetWaypoints()) do
self.enemy.Humanoid:MoveTo(waypoint.Position)
self.enemy.Humanoid.MoveToFinished:Wait()
end
-- Since the loop yields, once the enemy reaches the position it plays the position tween
positionTween:Play()
-- Cleanup
routeBlocked:Disconnect()
-- If waypoint is the last waypoint, set it to 0 (because next instruction is to add 1)
-- Else, just sets it to the waypoint
path.waypoint = (path.waypoint == path.maxWaypoints) and 0 or path.waypoint
path.waypoint += 1
end
This, of course, isn’t the best way to do it (I never really messed with pathfinding). However, if it works, don’t touch it! spoke like a true programmer right there
Next, we’ll put the logic that controls the method on our EnemyService
:
function EnemyService:Start()
-- Loop through the enemies's models to create new Enemy objects
for enemyIndex: number, enemy: Model in ipairs(enemyFolder) do
-- Create an instance of the Enemy class
local newEnemy = Enemy.new(enemy)
local enemyName = enemy.Name .. enemyIndex
-- Create a key for our new Enemy object
enemies[enemyName] = newEnemy
-- Inject pathfinding properties
newEnemy.path = {
waypoints = waypoints[enemyName]:GetChildren();
maxWaypoints = #waypoints[enemyName]:GetChildren();
waypoint = 1;
}
-- Set network ownership of the limbs to the server, so pathfinding isn't laggy
-- Coroutine'd, because why not?
coroutine.wrap(function()
for _, part: BasePart in ipairs(enemy:GetChildren() :: Array<BasePart>) do
if (not part:IsA("BasePart")) then continue end
part:SetNetworkOwner(nil)
end
end)()
end
-- Infinite loop 0_0
-- This is just a joke, if you're disturbed by this and you're copy pasting from the tutorial itself just change it to true (in the files I put true)
-- Why did I even say that? Do what you want!
while (not ("" == 0)) do
-- Coroutine'd or else they'd have to wait for the previous enemy to move
coroutine.wrap(function()
for _: string, enemy: Enemy in pairs(enemies) do
enemy:MoveToNextPoint()
end
end)()
wait(10)
end
end
And with that, let’s appreciate our creation again:
I swear I’m not that bad at stealth games to get caught like that