ThirdBlog-Top

Thursday, July 2, 2015

When the bot is complete (2)


                -- We found nothing so we will set our enemy as nil ( nothing ) and return false
                self:SetEnemy( nil )
                return false
end

----------------------------------------------------
-- ENT:RunBehaviour()
-- This is where the meat of our AI is
----------------------------------------------------
function ENT:RunBehaviour()
                -- This function is called when the entity is first spawned. It acts as a giant loop that will run as long as the NPC exists
                while ( true ) do
                                -- Lets use the above mentioned functions to see if we have/can find a enemy
                                if ( self:HaveEnemy() ) then
                                                -- Now that we have an enemy, the code in this block will run
                                                self.loco:FaceTowards( self:GetEnemy():GetPos() )        -- Face our enemy
                                                self:PlaySequenceAndWait( "plant" )                     -- Lets make a pose to show we found a enemy
                                                self:PlaySequenceAndWait( "hunter_angry" )-- Play an animation to show the enemy we are angry
                                                self:PlaySequenceAndWait( "unplant" )                -- Get out of the pose
                                                self:StartActivity( ACT_RUN )                                     -- Set the animation
                                                self.loco:SetDesiredSpeed( 450 )                              -- Set the speed that we will be moving at. Don't worry, the animation will speed up/slow down to match
                                                self.loco:SetAcceleration( 900 )                                 -- We are going to run at the enemy quickly, so we want to accelerate really fast
                                                self:ChaseEnemy()                                                                                          -- The new function like MoveToPos.
                                                self.loco:SetAcceleration( 400 )                                 -- Set this back to its default since we are done chasing the enemy
                                                self:PlaySequenceAndWait( "charge_miss_slide" )          -- Lets play a fancy animation when we stop moving
                                                self:StartActivity( ACT_IDLE )                                      --We are done so go back to idle
                                                -- Now once the above function is finished doing what it needs to do, the code will loop back to the start
                                                -- unless you put stuff after the if statement. Then that will be run before it loops
                                else
                                                -- Since we can't find an enemy, lets wander
                                                -- Its the same code used in Garry's test bot
                                                self:StartActivity( ACT_WALK )                                   -- Walk anmimation
                                                self.loco:SetDesiredSpeed( 200 )                              -- Walk speed
                                                self:MoveToPos( self:GetPos() + Vector( math.Rand( -1, 1 ), math.Rand( -1, 1 ), 0 ) * 400 ) -- Walk to a random place within about 400 units ( yielding )
                                                self:StartActivity( ACT_IDLE )
                                end
                                -- At this point in the code the bot has stopped chasing the player or finished walking to a random spot
                                -- Using this next function we are going to wait 2 seconds until we go ahead and repeat it
                                coroutine.wait( 2 )

                end

end

----------------------------------------------------
-- ENT:ChaseEnemy()
-- Works similarly to Garry's MoveToPos function
-- except it will constantly follow the
-- position of the enemy until there no longer
-- is one.
----------------------------------------------------
function ENT:ChaseEnemy( options )

                local options = options or {}

                local path = Path( "Follow" )
                path:SetMinLookAheadDistance( options.lookahead or 300 )
                path:SetGoalTolerance( options.tolerance or 20 )
                path:Compute( self, self:GetEnemy():GetPos() )                              -- Compute the path towards the enemies position

                if ( !path:IsValid() ) then return "failed" end

                while ( path:IsValid() and self:HaveEnemy() ) do

                                if ( path:GetAge() > 0.1 ) then                                                                    -- Since we are following the player we have to constantly remake the path
                                                path:Compute( self, self:GetEnemy():GetPos() )-- Compute the path towards the enemy's position again
                                end
                                path:Update( self )                                                                                                                         -- This function moves the bot along the path

                                if ( options.draw ) then path:Draw() end
                                -- If we're stuck, then call the HandleStuck function and abandon
                                if ( self.loco:IsStuck() ) then
                                                self:HandleStuck()
                                                return "stuck"
                                end

                                coroutine.yield()

                end

                return "ok"

end

list.Set( "NPC", "simple_nextbot", {
                Name = "Simple bot",
                Class = "simple_nextbot",
                Category = "NextBot"

} )

When the bot is complete..


Now that the bot is complete you will want to add it to the NPC spawn tab. Pretty easy, but here is the code used for this tutorial anyways:

list.Set( "NPC", "simple_nextbot", {
                Name = "Simple bot",
                Class = "simple_nextbot",
                Category = "NextBot"
} )
When you use this just replace both "simple_nextbot" with the file name of the bot and the rest is pretty easy to figure out yourself.

Challenges

You now have a basic bot running around the map and that's pretty much it. Here are some things you can try on your own to spice it up:

Search for more then just players
Play sounds when its wandering around.
Only search for enemies that are in front of it, not all around.
Make it hide if the enemy is holding a shotgun.
Stop chasing the enemy when it's really close and do a melee attack.
The full code

AddCSLuaFile()

ENT.Base                                             = "base_nextbot"
ENT.Spawnable                = true

function ENT:Initialize()

                self:SetModel( "models/hunter.mdl" )

                self.LoseTargetDist         = 2000   -- How far the enemy has to be before we lose them
                self.SearchRadius            = 1000   -- How far to search for enemies

end

----------------------------------------------------
-- ENT:Get/SetEnemy()
-- Simple functions used in keeping our enemy saved
----------------------------------------------------
function ENT:SetEnemy( ent )
                self.Enemy = ent
end
function ENT:GetEnemy()
                return self.Enemy
end

----------------------------------------------------
-- ENT:HaveEnemy()
-- Returns true if we have a enemy
----------------------------------------------------
function ENT:HaveEnemy()
                -- If our current enemy is valid
                if ( self:GetEnemy() and IsValid( self:GetEnemy() ) ) then
                                -- If the enemy is too far
                                if ( self:GetRangeTo( self:GetEnemy():GetPos() ) > self.LoseTargetDist ) then
                                                -- If the enemy is lost then call FindEnemy() to look for a new one
                                                -- FindEnemy() will return true if an enemy is found, making this function return true
                                                return self:FindEnemy()
                                -- If the enemy is dead( we have to check if its a player before we use Alive() )
                                elseif ( self:GetEnemy():IsPlayer() and !self:GetEnemy():Alive() ) then
                                                return self:FindEnemy()                               -- Return false if the search finds nothing
                                end
                                -- The enemy is neither too far nor too dead so we can return true
                                return true
                else
                                -- The enemy isn't valid so lets look for a new one
                                return self:FindEnemy()
                end
end

----------------------------------------------------
-- ENT:FindEnemy()
-- Returns true and sets our enemy if we find one
----------------------------------------------------
function ENT:FindEnemy()
                -- Search around us for entities
                -- This can be done any way you want eg. ents.FindInCone() to replicate eyesight
                local _ents = ents.FindInSphere( self:GetPos(), self.SearchRadius )
                -- Here we loop through every entity the above search finds and see if it's the one we want
                for k, v in pairs( _ents ) do
                                if ( v:IsPlayer() ) then
                                                -- We found one so lets set it as our enemy and return true
                                                self:SetEnemy( v )
                                                return true
                                end

                end

NextBot NPC Creation


Nextbot is what the AI in Team Fortress 2 and Left 4 Dead use. This tutorial will go over the steps for a simple AI that will search for enemies (you) and chase them until they die or are too far away. It will also do some random other stuff when there are not any enemies.

Lets get started

First create the .lua file for your entity. The one I made for this tutorial is in "addons/Nextbot_tut/lua/entities" and is named "simple_nextbot.lua". Now open that file so you can start adding the code.

The code

The basic stuff we need for entities

Start off with defining the base entity to use and making it spawnable. Pretty much the same as any other entity so far Here we set the model and define some variables we will use later.

AddCSLuaFile()

ENT.Base                                             = "base_nextbot"
ENT.Spawnable                = true

function ENT:Initialize()

                self:SetModel( "models/hunter.mdl" )

                self.LoseTargetDist         = 2000   -- How far the enemy has to be before we lose them
                self.SearchRadius            = 1000   -- How far to search for enemies

end
Enemy related stuff

This adds some useful functions for enemy related stuff. An NPC/bot isn't complete if it can't target stuff, right? These include a function to check if there is still an enemy or if it got away and a function to search for enemies. I've added all sorts of comments so you know exactly what they do.

----------------------------------------------------
-- ENT:Get/SetEnemy()
-- Simple functions used in keeping our enemy saved
----------------------------------------------------
function ENT:SetEnemy( ent )
                self.Enemy = ent
end
function ENT:GetEnemy()
                return self.Enemy
end

----------------------------------------------------
-- ENT:HaveEnemy()
-- Returns true if we have an enemy
----------------------------------------------------
function ENT:HaveEnemy()
                -- If our current enemy is valid
                if ( self:GetEnemy() and IsValid( self:GetEnemy() ) ) then
                                -- If the enemy is too far
                                if ( self:GetRangeTo( self:GetEnemy():GetPos() ) > self.LoseTargetDist ) then
                                                -- If the enemy is lost then call FindEnemy() to look for a new one
                                                -- FindEnemy() will return true if an enemy is found, making this function return true
                                                return self:FindEnemy()
                                -- If the enemy is dead( we have to check if its a player before we use Alive() )
                                elseif ( self:GetEnemy():IsPlayer() and !self:GetEnemy():Alive() ) then
                                                return self:FindEnemy()                               -- Return false if the search finds nothing
                                end
                                -- The enemy is neither too far nor too dead so we can return true
                                return true
                else
                                -- The enemy isn't valid so lets look for a new one
                                return self:FindEnemy()
                end
end

----------------------------------------------------
-- ENT:FindEnemy()
-- Returns true and sets our enemy if we find one
----------------------------------------------------
function ENT:FindEnemy()
                -- Search around us for entities
                -- This can be done any way you want eg. ents.FindInCone() to replicate eyesight
                local _ents = ents.FindInSphere( self:GetPos(), self.SearchRadius )
                -- Here we loop through every entity the above search finds and see if it's the one we want
                for k, v in pairs( _ents ) do
                                if ( v:IsPlayer() ) then
                                                -- We found one so lets set it as our enemy and return true
                                                self:SetEnemy( v )
                                                return true
                                end
                end
                -- We found nothing so we will set our enemy as nil ( nothing ) and return false
                self:SetEnemy( nil )
                return false
end
Coroutines


Now that the bot can find enemies we need to get it to actually do something other then just having an enemy. This next part is where most of our AI will be set up. The function is a coroutine, or pretty much a giant looping section of code, except you can pause it for a period of time using coroutine.wait( time ). Coroutines allow you to do things in a timed order, letting you pause the function so we can make the bot face the player or play an animation. And since its all inside a while true loop, it will run for as long as the bot exists. So after your ai has finished running everything it can do, it will go back and do it again. Here is an example of a very simple bot.

Using Viewmodel Hands



Scripted Weapons

To make your SWEP use the viewmodel hands, add this code:

SWEP.UseHands = true
Note that your SWEP must also use a C model for its viewmodel:

SWEP.ViewModel = "models/weapons/c_smg1.mdl"
Custom Gamemodes

If you want to use the Viewmodel Hands system in your gamemode, you must put some code into your gamemode.

Without Player Classes

Serverside: ( This MUST go into a GM:PlayerSpawn function, paste it to the end )

function GM:PlayerSpawn( ply )
                -- Your code
                ply:SetupHands() -- Create the hands and call GM:PlayerSetHandsModel
end

-- Choose the model for hands according to their player model.
function GM:PlayerSetHandsModel( ply, ent )

                local simplemodel = player_manager.TranslateToPlayerModelName( ply:GetModel() )
                local info = player_manager.TranslatePlayerHands( simplemodel )
                if ( info ) then
                                ent:SetModel( info.model )
                                ent:SetSkin( info.skin )
                                ent:SetBodyGroups( info.body )
                end

end
Using Player Classes

Serverside: ( Again, must be put into GM:PlayerSpawn after you set players class )

function GM:PlayerSpawn( ply )
                -- Set your player class

                ply:SetupHands() -- Create the hands and call GM:PlayerSetHandsModel
end
By default GM:PlayerSetHandsModel calls GetHandsModel of players current player class.

Clientside code

If you haven't overridden GM:PostDrawViewModel, this step is optional.
This code makes the hands draw. It must be clientside.

function GM:PostDrawViewModel( vm, ply, weapon )

                if ( weapon.UseHands || !weapon:IsScripted() ) then

                                local hands = LocalPlayer():GetHands()
                                if ( IsValid( hands ) ) then hands:DrawModel() end

                end

end
Player Class

If you want a specific player class to use specific hands then add this function.

function PLAYER:GetHandsModel()

                return { model = "models/weapons/c_arms_cstrike.mdl", skin = 1, body = "0100000" }

end
Player Models

If you're adding a player model and want to add custom hands then use this code.

player_manager.AddValidHands( "css_arctic", "models/weapons/c_arms_cstrike.mdl", 0, "00000000" )
Where 'css_arctic' is the name you used when calling player_manager.AddValidModel — the two numbers are respectively the skin and bodygroup values.

If a player model doesn't have a corresponding hands entry, it will default to the civilian hands.

Model creators

Here's an example c_model that shows how things need to be set up in your QC and SMD files. This is the source files of the medkit SWEP that is packed by default with Gmod.

https://github.com/robotboy655/gmod-animations/raw/master/c_medkit.7z

Here are the source files for the c_arms_citizen model, if you need some reference for compiling arms:

https://github.com/robotboy655/gmod-animations/raw/master/c_arms_citizen.7z

Here's the citizen arms on a CAT animation rig.

https://github.com/robotboy655/gmod-animations/raw/master/3DS2011_CAT_c_arms_citizen.max

If you need SMD files for these arms (making your own animation rigs, etc.), here they are.

https://github.com/robotboy655/gmod-animations/raw/master/c_arms_citizen.smd

https://github.com/robotboy655/gmod-animations/raw/master/c_arms_combine.smd

The arms mesh should never be in the weapon model itself; that defeats the entire purpose of the system!

Remember to include c_arms_definebones.qci from the medkit example, as otherwise StudioMDL will optimise away the arm bones that you've included and then it won't work.


Gmod classes for player

Player Classes

Player classes can be changed and swapped at runtime. They change things like the player's speed, height, the weapons the player spawns with, what happens when they die, what happens when they spawn etc.

Imagine a Team Fortress 2 gamemode. You want the Scout to run faster than the HWG. So you would design different classes for them. When a player spawns you set their class - and they have those attributes.

Player classes can be derived. So your gamemode can define a shared common class for all of your players and derive different classes from there.

Why

Traditionally if you wanted to do this in your gamemode you would either code a system similar to this, or you would end up with a bunch of if/else blocks in your spawn functions.

This system cleans that up by making it unified and object orientated.

Setting a player's class

Setting a player's class is done easily. You only need to do it serverside.

player_manager.SetPlayerClass( ply, "player_sandbox" )
This is commonly done in the PlayerSpawn gamemode hook - but you can call it anywhere you want.

A Player Class

The player class itself doesn't need much comment. It should be mostly self explanatory.

The class below creates a new player class called "player_custom", which is derived from "player_default". The player spawns with a pistol.

DEFINE_BASECLASS( "player_default" )

local PLAYER = {}

--
-- See gamemodes/base/player_class/player_default.lua for all overridable variables
--
PLAYER.WalkSpeed                                         = 200
PLAYER.RunSpeed                                                           = 400

function PLAYER:Loadout()

                self.Player:RemoveAllAmmo()

                self.Player:GiveAmmo( 256,       "Pistol",                                true )
                self.Player:Give( "weapon_pistol" )

end

player_manager.RegisterClass( "player_custom", PLAYER, "player_default" )
These player class functions are usually housed in gamemode/player_class/ - and they do not get loaded automatically. You should load it in your shared.lua gamemode file. And they should be loaded in the right order - so that base classes get loaded before their children.


Creat Gamemode content (Part2) (Gmod)

Menu Background Images

If you want to include backgrounds with your gamemode place them in gamemodes/<yourgamemode>/backgrounds/. They must all be jpg images.

Menu Logo Image

To supply a menu logo image place a png file in gamemodes/<yourgamemode>/logo.png. This image should be 128 pixels high and up to 1024 pixels wide. The default logo is 288 x 128.

Menu Icon

To supply a menu icon place a png file in gamemodes/<yourgamemode>/icon24.png. This image should ideally be 24x24 pixels but it can be up to 32x32. The server list displays the icons at 24x24 while the gamemode selection uses 32x32. Here's where it will appear.

MenuIcons.jpg
Init and Shared Files

Three files are needed for your gamemode to function properly. These go into your <gamemode>/gamemode/ folder.

Please note, that while we use shared.lua in the example below, it is completely optional. init.lua and cl_init.lua are not!

init.lua

This file is called when the server loads the gamemode. The example below tells the resource system to send the two files to the client, and then loads shared.lua.

AddCSLuaFile( "cl_init.lua" )
AddCSLuaFile( "shared.lua" )

include( "shared.lua" )
shared.lua

This file sets some common variables and creates the Initialize function. This is the only function you NEED to define. All others will be called from the base gamemode.

GM.Name = "Super Killers"
GM.Author = "N/A"
GM.Email = "N/A"
GM.Website = "N/A"

function GM:Initialize()
                -- Do stuff
end
cl_init.lua

This file is called when the client loads the gamemode. You would put client specific things in here. We load shared.lua. Note that you can only 'include' files here that have been 'AddCSLuaFile'd on the server.

include( "shared.lua" )
Deriving Gamemodes

If you are deriving your gamemode from something other than base, use this function to tell the engine that we do.

You must call the function on both, client and server, and since our shared.lua is included from both, cl_init.lua and init.lua, we can do it once, in shared.lua.

In the example below, let's say we derive our gamemode from sandbox, so we add this to our shared.lua, somewhere above all function definitions is a good place:

DeriveGamemode( "sandbox" )
Common Errors

Error loading gamemode: info.Valid

Your Gamemode Text File is invalid or was not found.

Error loading gamemode: !IsValidGamemode

The game could not find cl_init.lua or init.lua of your gamemode.

Game crashes/freezes on death

You have not assigned a playermodel to the player.



Gamemode creation (GMOD)


Gamemode Content

Gamemode content such as maps, models and sounds should be placed in gamemodes/content/ folder. (ie gamemodes/content/models/..).

Gamemode Folder

To create a new gamemode you need to create a new folder in the gamemodes folder. The folder should take the name of your gamemode. Inside of that folder there should be a text file named whatever your folder is.

For instance, if my gamemode was called "SuperKillers", I would create a folder in "garrysmod/gamemodes/" called "superkillers". Then inside that folder I would create a text file called "superkillers.txt".

Note that in previous versions of Garry's Mod this file was always called "info.txt" - but was changed in Version 13 to reflect the changes to the content.

Gamemode Text File

The .txt file you created needs to be a keyvalues file.

"superkillers"
{
                "base"                  "base"
                "title"                    "Super Killers"
                "maps"                 "^sk_"
                "menusystem" "1"
                "workshopid"    "15895"

                "settings"
                {
                                1
                                {
                                                "name"                "sk_maxfrags"
                                                "text"                    "Max Frags"
                                                "help"                   "The maxiumum number of frags before a map change"
                                                "type"                   "Numeric"
                                                "default"             "20"
                                }


                                2
                                {
                                                "name"                "sk_grenades"
                                                "text"                    "Allow Grenades"
                                                "help"                   "If enabled then grenades are enabled"
                                                "type"                   "CheckBox"
                                                "default"             "1"
                                }

                }

}
key         description
base      The gamemode you're deriving from. This will usually be base or sandbox.
title        The 'nice' title of your gamemode. Can contain spaces and capitals.
maps     The map filter for your gamemode. This is used to correctly categorise maps in the map selector. This should only be set if the maps are unique to your gamemode.
menusystem     Include and set to 1 if this gamemode should be selectable from the main menu
workshopid        Optional. If your gamemode is on Workshop then this should be the workshopid of the file. (You won't have a workshopid until you upload - so you will need to set this in an update)
settings                See Below
Settings

The settings table allows you to create server configuration convars for your gamemode. It's completely optional. Your text file doesn't have to have a settings section.

In previous versions of GMod this section used to have its own file, in gamemode/<gamemode>/content/settings/server_settings/. It was moved to the root folder to decrease load times.

These settings are configurable in the main menu in GMod. Each entry has a number of fields.


key         description
name    The name of the convar
text        The text to show in game to describe this convar
help       The help text to show on the convar (in the console)
type       Either Text, CheckBox or Numeric. These are case-sensitive!

default The default value for the convar