Jump to content

Basic sneaking tutorial


thehankinator
 Share

Recommended Posts

*UPDATE* 6/22/15 I've added code for simple peeking functionality.

 

This is a tutorial on how to implement a rudimentary sneaking mechanic to your game. The monster will only see the player if they in front of it, while taking into consideration of the height of the player. This means the player could crouch behind an object, not under one. I assume you are comfortable and familiar with lua script and Leadwerks 3.5, therefore I will not be covering lua syntax nor how to do basic things in Leadwerks. The code samples below will have the new code surrounded by comments, the rest is there just to give you reference to what section of the code we are working in.

 

I broke this problem down into two sub tasks.

  1. Crouch to FPSPlayer
  2. Peek to FPSPlayer
  3. Field of view check to MonsterAI

Initial setup

Open your project. In the asset tab, navigate to the FPSPlayer.lua script. Make a copy of it, call it FPSPlayerSneak.lua. Next at the top of FPSPlayerSneak.lua, we need to add the variables that we will need.

 

Script.mouseDifference = Vec2(0,0)
Script.playerMovement = Vec3(0,0,0)
Script.tempJumpForce = 0

--CROUCH BEGIN
Script.crouched = false
Script.crouchheight = 1.0
--CROUCH END

--PEEK BEGIN
Script.peek_translate = 0
Script.peek_distance = 0.5
--PEEK END

function Script:CycleWeapon(direction)

 

Script.crouched

This flag indicates if the player is currently crouching. You could set this flag if you wanted the player to start crouched.

 

Script.crouchheight

This is the height of the player when crouching. You could adjust this if you wanted the player to be taller or shorter while crouched

 

Script.peek_translate

This variable keeps track of how far we are peeking, no need to play with this one

 

Script.peek_distance

Defines the distance the player can peek, increase it to lean further

 

Add crouch

Toward the bottom of the UpdatePhysics function we have two things that need to be done.

 

First, check for the control key then toggle a flag indicating if the player should be crouched or not.

--Give the player an extra boost when jumping
playerMovement = playerMovement * 1.6
end

--CROUCH BEGIN
-- Check for crouching
if window:KeyHit(Key.ControlKey) then
self.crouched = not self.crouched
end
--CROUCH END

--With smoothing
--Position camera at correct height and playerPosition
self.entity:SetInput(self.camRotation.y, playerMovement.z, playerMovement.x, jump , false, 1.0, 0.5, true)

 

A few lines down, we need to position the camera to the required height based on the crouch flag

newCameraPos = Vec3(playerPos.x, newCameraPos.y, playerPos.z)

--CROUCH BEGIN
local target_height

--determine the height of the camera
if self.crouched then
target_height = self.crouchheight
else
target_height = self.eyeheight
end

--calculate the new camera position
if newCameraPos.y<playerPos.y + target_height then
newCameraPos.y = Math:Curve(playerPos.y + target_height, newCameraPos.y, self.camSmoothing)
else
newCameraPos.y = playerPos.y + target_height
end
--CROUCH END

self.camera:SetPosition(newCameraPos)

 

Add peek

We will continue working in FPSPlayerSneak.lua First we need to remap the E key from use to something else, for the sake of this tutorial I am going to use G. Scroll down the UpdateWorld() function and free up that E key.

--PEEK BEGIN
if window:KeyHit(Key.G) then
--PEEK END
if self.carryingEntity then
self:DropEntityCarrying()

Now that both the Q and E keys are available to use, we need to check them then set the direction we are going to shift the camera.

 

-- Check for crouching
if window:KeyHit(Key.ControlKey) then
self.crouched = not self.crouched
end
--CROUCH END

--PEEK BEGIN
if window:KeyDown(Key.Q) then
self.peek_translate = Math:Curve(-self.peek_distance, self.peek_translate, self.camSmoothing)
elseif window:KeyDown(Key.E) then
self.peek_translate = Math:Curve(self.peek_distance, self.peek_translate, self.camSmoothing)
else
self.peek_translate = 0
end
--PEEK END

--With smoothing

 

Finally at the bottom of this function we are going to translate the camera to the side in the direction we are peeking

 

self.camera:SetPosition(newCameraPos)

--PEEK BEGIN
self.camera:SetPosition(Transform:Point(self.peek_translate,0,0,self.camera,nil))
--PEEK END
end

 

Field of view check to MonsterAI

In the asset tab, navigate to the MonsterAI.lua script. Make a copy of it, call it MonsterAIFOV.lua. Open that up and find the ChooseTarget() function.

 

local d = self.entity:GetDistance(entity)
local pickinfo=PickInfo()

--LOS BEGIN
local entity_pos

if entity.script.camera ~= nil then--if there is a camera object
 entity_pos = entity.script.camera:GetPosition(true)--use the camera position
else
 entity_pos = entity:GetPosition(true)--otherwise, use the entity's position
end

--cast a ray from the monster's eye to the target position
if self.entity.world:Pick(self.entity:GetPosition()+Vec3(0,1.6,0),entity_pos,pickinfo,0,false,Collision.LineOfSight)==false then
 --when we reach here, the target is in line of sight

 --now we need to check if the target is in the monster's field of view.
 --we are going to measure the angle from the direction the monster is looking
 --to the target.
 local dir_facing = Transform:Normal(0,0,1,self.entity,nil):Normalize()
 local dir_to_target = (entity:GetPosition() - self.entity:GetPosition()):Normalize()
 local angle = math.acos(dir_facing:Dot(dir_to_target))
local window=Window:GetCurrent()
 --compare the resulting angle in radians, this determines the monster's field of view
--or if the player is running(thanks nick.ace!)
 if angle > 2.0943951 or window:KeyDown(Key.Shift) then --~120 or degees
 return entity.script
 end
end
--LOS END
end

 

The math in this section is kind of confusing. I got adapted the formulas from the following website.

 

http://blog.wolfire.com/2009/07/linear-algebra-for-game-developers-part-2/

 

Wrap up

Set your monster to use MonsterAIFOV.lua, this should give you the FOV check. Then, set your player object to use FPSPlayerCrouch.lua, this will allow you to crouch when you press the control key.

Feedback welcome.

  • Upvote 3
Link to comment
Share on other sites

Yeah, just add another condition to it:

--compare the resulting angle in radians, this determines the monster's field of view
local window=Window:GetCurrent()
if angle > 2.0943951 or window.KeyDown(Key.Shift)  then --~120 degees
 return entity.script
end

 

You may even want to put the sprint detection code somewhere else under different conditions, but you'll need to decide what you want.

  • Upvote 2
Link to comment
Share on other sites

Yeah, just add another condition to it:

--compare the resulting angle in radians, this determines the monster's field of view
local window=Window:GetCurrent()
if angle > 2.0943951 or window.KeyDown(Key.Shift) then --~120 degees
return entity.script
end

 

You may even want to put the sprint detection code somewhere else under different conditions, but you'll need to decide what you want.

Great solution, my first ideas were far more complex.

Link to comment
Share on other sites

This line should be changed from:

if self.entity.world:Pick(self.entity:GetPosition()+Vec3(0,1.6,0),entity_pos,pickinfo,0,false,Collision.LineOfSight)==false then

to

if self.entity.world:Pick(self.entity:GetPosition()+Vec3(0,1.6,0),entity_pos,pickinfo,0,true,Collision.LineOfSight)==false then

 

The closest flag should be true because otherwise the raycast could pick anything on that ray instead of just the closest. Also, the height isn't changing, so you need to account for that.

 

local pick_height=self.eyeheight
if self.crouched then pick_height=self.crouchheight end

if self.entity.world:Pick(self.entity:GetPosition()+Vec3(0,pick_height,0),entity_pos,pickinfo,0,true,Collision.LineOfSight)==false then

 

This will change the height at which the pick will end.

Link to comment
Share on other sites

The closest flag should be true because otherwise the raycast could pick anything on that ray instead of just the closest. Also, the height isn't changing, so you need to account for that.

 

local pick_height=self.eyeheight
if self.crouched then pick_height=self.crouchheight end

if self.entity.world:Pick(self.entity:GetPosition()+Vec3(0,pick_height,0),entity_pos,pickinfo,0,true,Collision.LineOfSight)==false then

 

This will change the height at which the pick will end.

 

I don't get where to put this :P

 

Here is a shadow/light detector for sneaking (cpp is needed to build your own exe to expose a function to lua)

http://www.leadwerks.com/werkspace/topic/11991-csg-shape-to-mimic-light/#entry87032

I'll take a look at it, thank you.

Link to comment
Share on other sites

local d = self.entity:GetDistance(entity)
local pickinfo=PickInfo()
--LOS BEGIN
local entity_pos
if entity.script.camera ~= nil then--if there is a camera object
	 entity_pos = entity.script.camera:GetPosition(true)--use the camera position
else
	 entity_pos = entity:GetPosition(true)--otherwise, use the entity's position
end
--cast a ray from the monster's eye to the target position
local pick_height=self.eyeheight
if self.crouched then pick_height=self.crouchheight end
if self.entity.world:Pick(self.entity:GetPosition()+Vec3(0,pick_height,0),entity_pos,pickinfo,0,true,Collision.LineOfSight)==false then
--when we reach here, the target is in line of sight

	 --now we need to check if the target is in the monster's field of view.
	 --we are going to measure the angle from the direction the monster is looking
	 --to the target.
	 local dir_facing = Transform:Normal(0,0,1,self.entity,nil):Normalize()
	 local dir_to_target = (entity:GetPosition() - self.entity:GetPosition()):Normalize()
	 local angle = math.acos(dir_facing:Dot(dir_to_target))

	 --compare the resulting angle in radians, this determines the monster's field of view
local window=Window:GetCurrent()
if angle > 2.0943951 or window.KeyDown(Key.Shift) then --~120 degees
return entity.script
end
end
--LOS END
end

Link to comment
Share on other sites

Nevermind, I forgot that self.crouched and the other variables are for the player. There's a few more steps you'll need to do:

 

Inside of the player's Start() method put this:

player=self.entity

This allows you to reference the player in any script.

 

Next, replace these values in the script above:

self.crouched should be player.script.crouched

self.crouchedheight should be player.script.crouchedheight

self.eyeheight should be player.script.eyeheight

 

That should work.

Link to comment
Share on other sites

Now the game starts, but when I crouch the same error pops up >_<

I'll put my code here, in case you find something that's wrong.

 

if entity.script.health>0 then
local d = self.entity:GetDistance(entity)
local pickinfo=PickInfo()
--LOS BEGIN
local entity_pos
if entity.script.camera ~= nil then--if there is a camera object
			 entity_pos = entity.script.camera:GetPosition(true)--use the camera position
else
			 entity_pos = entity:GetPosition(true)--otherwise, use the entity's position
end
--cast a ray from the monster's eye to the target position
local pick_height=player.script.eyeheight
if player.script.crouched then pick_height=player.script.crouchedheight end
if self.entity.world:Pick(self.entity:GetPosition()+Vec3(0,pick_height,0),entity_pos,pickinfo,0,true,Collision.LineOfSight)==false then
			 --when we reach here, the target is in line of sight
			 --now we need to check if the target is in the monster's field of view.
			 --we are going to measure the angle from the direction the monster is looking
			 --to the target.
			 local dir_facing = Transform:Normal(0,0,1,self.entity,nil):Normalize()
			 local dir_to_target = (entity:GetPosition() - self.entity:GetPosition()):Normalize()
			 local angle = math.acos(dir_facing:Dot(dir_to_target))

			 --compare the resulting angle in radians, this determines the monster's field of view
local window=Window:GetCurrent()
			 if angle > 2.0943951 then --~120 degees
			 return entity.script
		 end
		 end
--LOS END
	 end
 end
end
end

Link to comment
Share on other sites

I found tiny problem, the monster can still see me when I'm crouched behind a big wall...

 

Is it possible your wall is not set to "Scene" in the physics tab? Collision.LineOfSight only collides with Scene objects.

 

This line should be changed from:

if self.entity.world:Pick(self.entity:GetPosition()+Vec3(0,1.6,0),entity_pos,pickinfo,0,false,Collision.LineOfSight)==false then

to

if self.entity.world:Pick(self.entity:GetPosition()+Vec3(0,1.6,0),entity_pos,pickinfo,0,true,Collision.LineOfSight)==false then

 

The closest flag should be true because otherwise the raycast could pick anything on that ray instead of just the closest. Also, the height isn't changing, so you need to account for that.

 

local pick_height=self.eyeheight
if self.crouched then pick_height=self.crouchheight end

if self.entity.world:Pick(self.entity:GetPosition()+Vec3(0,pick_height,0),entity_pos,pickinfo,0,true,Collision.LineOfSight)==false then

 

This will change the height at which the pick will end.

 

The Pick() is checking for anything between the two points then only does the FOV check if there was nothing so the closest flag should not matter.

 

The height of the monster shouldn't be changing (in this tutorial anyway) but it is casting the ray to the camera. When the player crouches, the camera's position changes. So there should be no need to do any additional checks.

  • Upvote 1
Link to comment
Share on other sites

Yeah sorry about that, I forgot. I usually test for character collision using the closest pick, but you obviously chose to do it a different way.

 

You are right though, the pick position height variable should be switched:

local enemyheight=1.6
if self.entity.world:Pick(self.entity:GetPosition()+Vec3(0,enemyheight,0),entity_pos+Vec3(0,pick_height,0),pickinfo,0,true,Collision.LineOfSight)==false then

Link to comment
Share on other sites

Yeah sorry about that, I forgot. I usually test for character collision using the closest pick, but you obviously chose to do it a different way.

 

You are right though, the pick position height variable should be switched:

local enemyheight=1.6
if self.entity.world:Pick(self.entity:GetPosition()+Vec3(0,enemyheight,0),entity_pos+Vec3(0,pick_height,0),pickinfo,0,true,Collision.LineOfSight)==false then

What's wrong with the Pick points? The p0 point is the monster's head, in the Crawler's case "self.entity:GetPosition()+Vec3(0,1.6,0)" pretty much nail's it. The p1 point (entity_pos) will be the exact position of the camera (ie the players head).

Link to comment
Share on other sites

What's wrong with the Pick points? The p0 point is the monster's head, in the Crawler's case "self.entity:GetPosition()+Vec3(0,1.6,0)" pretty much nail's it. The p1 point (entity_pos) will be the exact position of the camera (ie the players head).

 

Oh I see. Sorry I kept getting confused with some of the points and stuff for some reason.

Link to comment
Share on other sites

I found out what was wrong, the monster can see the player through models, but not CSG brushes.

I was able to get this working. To test I used "cryogen_propb.mdl" from the scifi construction kit. The trick was that the editor would drop this model in as a Prop collision type. To fix this I went to the Scene tab, found the model (in my case it was called "cryogen_propb") expanded it to expose it's children. From there I found a child called "Form162"(yours will probably have a different name), when I selected it the whole model turned red in the editor viewports. It was this object that I changed the Collision type to Scene under the Physics tab. It seems like you have to specifically select the correct node and change this setting. I tried selecting the parent and all children and it wouldn't work.

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

 Share

×
×
  • Create New...