CraftStudio Wiki
CraftStudio Wiki

Add enemies[]

We can move, turn and shoot, now, we'll add enemies to shoot at!

The addition of enemies in the game will be the same as for our shots: we will define an enemy and his behavior in a prefabricated scene and then it this scene in our main scene when necessary.

Creation of our prefab enemy[]

Start by creating a new scene called "Prefab_enemy" add an object at (0,0,0), associate an enemy model (again in my case a cube with a different texture) and finally associate a new script named "enemy_IA." This will be our enemy artificial intelligence.

Instead of a real intelligence,our enemy will always move toward the player.

In the script enemy_IA's  Awake(), let's get our player object :

self.target = CraftStudio.FindGameObject( "Cube" )

We define the enemy movement speed :

self.speed = 0.3

Then in update() we will just make the enemy face the player and move in this direction :

local targetPos = self.target.transform:GetPosition()

self.gameObject.transform:LookAt( targetPos )
self.gameObject.transform:MoveOriented( Vector3:Forward()*self.speed )


Enemies spawning[]

It remains to spawn our enemies: In the main "game" scene, add a new script "spawn_enemies" linked to the object "map".In Awake() we will create a new object "Enemies" that will contains all the enemies that we will create later. It will allows us to easily find them later :

self.ennemiesRoot = CraftStudio.CreateGameObject( "Ennemies" )

Then, in Update() we create a new enemy linked to the root object :

CraftStudio.AppendScene( CraftStudio.FindAsset( "Prefab_enemy" ), self.ennemiesRoot )

This will spawn an enemy every game cycle... it will be a lot ! We will add a delay to spawn one just every 5 seconds :

function Behavior:Awake()
   self.ennemiesRoot = CraftStudio.CreateGameObject( "Enemies" )
   self.spawntimer = 0
end

function Behavior:Update()
   self.spawntimer = self.spawntimer+1
   
   if self.spawntimer == 60*5 then  
       CraftStudio.AppendScene( CraftStudio.FindAsset( "Prefab_enemy" ), self.ennemiesRoot )
       self.spawntimer = 0
   end

end

Start your game and test it. After 5 seconds, an enemy should appear .... in the ground, always in the same place, then move towards the player and follow him.

We will change the creation of our enemies so that they always appear at some distance of the player. In awake(), define a spawning distance :

self.spawnDist = 10

Then in Update(), we get the player's position:

local TargetPos = self.target.transform:GetPosition()

And define the position where it appear by shifting the of the player position to the north. Then reposition our enemy :

self.spawPos= TargetPos + Vector3:Forward() * self.spawnDist
self.gameObject.transform:SetPosition(self.spawPos)

Launch, test...

Enemies now appear always at the same position relative to the player (north since we used Vector3:Forward ().)

We will add some randomization using a random number generator.

The generator is part of the object "math" and needs to be initialized by the method math.randomseed(seed). The seed is a value from which the next numbers will be generated. Same seed always gives the same sequence of numbers (our generator is actually "pseudo-random") In order to have a different game every time, we use the time being as seed:

math.randomseed (os.time())

We will use our generator to select a random angle between 0 and 360 degrees

local angle = math.random(0, 359)

Then we rotate the forward vector by the our angle found : Vector3.Transform(Vector3:Forward(), Rotation Quaternion)

The rotation quaternion is a mathematical object that describes the rotation. We can create It from the rotation angle and the axis around which rotate (Y axis upwards): Quaternion:FromAxisAngle(Vector3:Up(), angle)

The starting position is :

self.spawPos = targetPos + Vector3.Transform( Vector3:Forward(), Quaternion:FromAxisAngle( Vector3:Up(), angle ) ) * SPAWN_DISTANCE

===Full Script : spawn_enemies :

function Behavior:Awake()
    math.randomseed(os.time())
    self.enemiesRoot = CraftStudio.CreateGameObject( "Enemies" )
    self.spawntimer = 0
end

function Behavior:Update()
    self.spawntimer = self.spawntimer+1

    if self.spawntimer == 60*5 then  
        CraftStudio.AppendScene( CraftStudio.FindAsset( "prefab_enemy" ), self.enemiesRoot )
        self.spawntimer = 0
    end
end

Enemy_AI :

function Behavior:Awake()
   self.speed = 0.03
   self.target = CraftStudio.FindGameObject( "Cube" ) --target

   --spawning  
   self.spawnDist = 10 --distance d'apparition

   local angle = math.random( 0, 359 )
   local targetPos = self.target.transform:GetPosition()

   self.spawPos = targetPos + Vector3.Transform( Vector3:Forward(), Quaternion:FromAxisAngle( Vector3:Up(), angle ) ) * self.spawnDist

   self.gameObject.transform:SetPosition( self.spawPos )
end

function Behavior:Update()
   --gestion du mouvement
   local targetPos = self.target.transform:GetPosition()
   self.gameObject.transform:LookAt( targetPos )
   self.gameObject.transform:MoveOriented( Vector3:Forward()*self.speed )
end


Kill the enemies ...[]

Now that our enemies are alive, we'll have to shoot them!

For that, we will need to determine when a bullet hits an enemy, and then destroy the bullet and the enemy.

We will ensure that this is the bullet that detects a collision with an enemy, but we could all do quite the opposite.

There are several techniques more or less complicated to detect collisions between two objects. Here we use the simplest and least accurate: the distance detection.

In our script "bullet", we will parse all existing enemies. For each enemy, we will check how far it is from the bullet.

In Update(), starts by retrieve the list of enemies present, remember that we have deliberately placed then in the same parent object "Enemies." We recover the children of the object "Enemies" with :

local enemies = CraftStudio.FindGameObject( "Enemies" ):GetChildren()

Then we also retrieve the actual position of the bullet :

local pos = self.gameObject.transform:GetPosition()

We will us a "for" statement to loop trough our list of enemies :

for i=1,#enemies do 
    local enemy = enemies[i] 
end

In the loop, for each enemies, we calculate the distance between the bullet and the enemy (enemy.transform:GetPosition()) with :

[Number] Vector3.Distance( [Vector3] v1, [Vector3] v2 )

It actually gives us us the distance between the center of both object (assuming your enemy is more or less square and his origin is at his center). If this distance is less than the radius of the enemy, then we will consider it touched. In my case, enemy is a cube of side 1, so I will consider its radius equal to 0.5. (Obviously collisions obtained will not be perfect, but it's a start...) We therefore test our collision with: if Vector3.Distance( enemy.transform:GetPosition(), pos ) < 0.5 then


end

And finally, if the collision occur, we just destroy the bullet :

CraftStudio.Destroy( self.gameObject )

We now need to destroy the smitten enemy. We could actually do it from the actual script (the one attached to the projectile), but as a principle, it's best to modify an object only from a script attached to it. This rule is obviously not absolute, but it will allow manage thing easily when your project will get bigger.

So, we will destroy our enemy from the script "Enemy_IA". To do it, in this one we will create a new function (a "method" to be exact):

function Behavior:Damage( )

end

This function define what happens when our enemy is hit by a bullet. For the time being, we will just destroy our enemy:

CraftStudio.Destroy( self.gameObject )

The Damage() method can be called within his own script "Enemy_IA" with self:Damage() or from within an other script with :

GameObject:SendMessage( [string] method name, [table] data )

Here, the used object will be the smitten enemy, the method name will be "Damage" and the data should be what we want to pass to this method as parameter (nothing here, but it could be used to the amount of damage for a particular weapon for example ). This way we get :

enemy:SendMessage( "Damage",nil)

And so the enemy's final script :

function Behavior:Awake()

  self.speed = 0.03
  self.target = CraftStudio.FindGameObject( "Cube" ) --cible à suivre

  --spawning position
  self.spawnDist = 10 --distance d'apparition
 
  math.randomseed(os.time())
  local angle = math.random( 0, 359 )
  local targetPos = self.target.transform:GetPosition()

  self.spawPos = targetPos + Vector3.Transform( Vector3:Forward(), Quaternion:FromAxisAngle( Vector3:Up(), angle ) ) * self.spawnDist
   
  self.gameObject.transform:SetPosition( self.spawPos )

end

function Behavior:Update()

  --handle movement
  local targetPos = self.target.transform:GetPosition()

  self.gameObject.transform:LookAt( targetPos )
  self.gameObject.transform:MoveOriented( Vector3:Forward()*self.speed ) 

end


--Method called when a bullet collide the enemy
function Behavior:Damage( )
   print ("ai")
   CraftStudio.Destroy( self.gameObject ) 
end

Bullet'script :

function Behavior:Awake()

   self.speed = 0.5
   self.MaxLife = 20
   self.source = CraftStudio.FindGameObject( "Cube" )
   
   --starting position
   self.gameObject.transform:SetPosition (self.source.transform:GetPosition())
   self.gameObject.transform:SetOrientation (self.source.transform:GetOrientation())
   
   --life initialization
   self.life = 0

end

function Behavior:Update()

  --handle movments
  self.gameObject.transform:MoveOriented( Vector3:Forward()*self.speed ) 
  
  --handle life span
  self.life = self.life +1
  if self.life > self.MaxLife then
       CraftStudio.Destroy( self.gameObject ) 
  end
  
  --handle colisions with enemies
  local enemies = CraftStudio.FindGameObject( "Enemies" ):GetChildren()
  local pos = self.gameObject.transform:GetPosition()
  
  for i=1,#enemies do
      local enemy = enemies[i]
                        
      if Vector3.Distance( enemy.transform:GetPosition(), pos ) < 0.5 then
           enemy:SendMessage( "Damage",nil)
           CraftStudio.Destroy( self.gameObject ) 
      end
  end

end

Earn points Edit[]

Now that we can destroy enemies, we will add a point management: In the character's management script, we   add a variable that will store our score:

self.score = 0

We will also create a method to add points. It takes as a parameter the number of points to add and simply add it to the current score and then display it in the debug window:

function Behavior:AddPoint(point)
   self.score = self.score + point
   print ("You have "..self.score.." points")
end

 It remains to define when AddPoint() will be called. It will be our enemies at the time of death which will give the player points. So in the "Ennemi_AI" script, add a script property (in the banner at the top of the script) in order to define the number of point given to the player. Call it "ScoreWhenKilled", defined as a number with 1 as default. Then in the function Damage() in "Ennemi_AI" script, add: self.target:SendMessage("AddPoint", self.ScoreWhenKilled) The contents of the property ScoreWhenKill, will be passed as a parameter to "AddPoint()" and thus added to the player's score.==And get killed...== Now that we can kill the enemies, we'll make sure that they can also killed us. We will use the same principle as for collisions between projectiles and enemies. In "ennemi_AI" script, Update() will test collisions with the player in the sae we did for the bullet:

local direction = targetPos - self.gameObject.transform:GetPosition()
local distance = direction:Length()
 
if distance < 1 then
       self.target:SendMessage( "Damage",nil)
       CraftStudio.Destroy( self.gameObject )
end

 Then, we create a new "Damage()" method in the player script to handle what happen when an enemy touch the player :

function Behavior:Damage( ) 
    CraftStudio.LoadScene (CraftStudio.FindAsset("menu")) 
end

Run your game and check that, if the enemy touches you, the game go back to the main menu.It's a little rough right? We will add a life bar to our character: in the player's script, in awake() add : self.life = 10  In "ennemi_AI" script  we will define how many hit damage cause the enemy when it touches the player. In order to change this value easily if you create several different types of enemies, we will create as script property. Create a property in script "ennemi_AI" named "power", it will be a number with 1 as default.In this same script change the called line when a collision with the player accur to have: self.target:SendMessage( "Damage",self.power) We will now change the player's "Damage()" method to reduce the player life level each time it collides an enemy. At the beginnig of the method, we add :

self.life = self.life -damage

We also add a line to show the remaining life point in the debug window :

print ("You still have "..self.life.."life points")

Et finelly we get back to the menu when the player is out of life points ::

if self.life <= 0 then
   CraftStudio.LoadScene (CraftStudio.FindAsset("menu"))
end

Final Script :[]

Player :

function Behavior:Awake()

   self.speed = 0.05
   self.life = 10
   self.score = 0
   
   self.groundPlane = Plane:New( Vector3:Up(), -1 )
   
   self.crosshair = CraftStudio.FindGameObject( "crosshair" )
   self.cameraComponent = CraftStudio.FindGameObject( "Camera" ):GetComponent( "Camera" )
   self.bulletPrefab = CraftStudio.FindAsset( "prefab_bullet" )
   

end

function Behavior:Update()
   --handle movement
   local horizontal = CraftStudio.Input.GetAxisValue( "Horizontal" )
   local vertical = CraftStudio.Input.GetAxisValue( "Vertical" )

   local movement = Vector3:New( horizontal, 0, -vertical )
   movement = movement * self.speed
   self.gameObject.transform:Move (movement)

   --handle orientation
   local mousePos = CraftStudio.Input.GetMousePosition()
   local mouseRay = self.cameraComponent:CreateRay( mousePos )
   local distance = mouseRay:IntersectsPlane( self.groundPlane )
   
   if distance ~= nil then
           local targetPos = mouseRay.position + mouseRay.direction * distance
           self.crosshair.transform:SetPosition( targetPos )
           self.gameObject.transform:LookAt( targetPos )
   end
   
   --handle firing
   if CraftStudio.Input.IsButtonDown( "Fire" ) then
       CraftStudio.Instantiate("bullet",self.bulletPrefab)
   end

end

--Method called when this object collide an enemy
function Behavior:Damage(damage)

   self.life = self.life -damage
   print ("You still have "..self.life.."life points")
   if self.life <= 0 then
       CraftStudio.LoadScene (CraftStudio.FindAsset("menu"))
   end 

end

--Method called when an enemy is killed
function Behavior:AddPoint(point)
   self.score = self.score + point
   print ("You have "..self.score.." points")

end

Enemy :

function Behavior:Awake()

  self.speed = 0.03
  self.radius = 1
  self.target = CraftStudio.FindGameObject( "Cube" ) --target

  --spawning
  self.spawnDist = 10 --distance d'apparition
 
  math.randomseed(os.time())
  local angle = math.random( 0, 359 )
  local targetPos = self.target.transform:GetPosition()

  self.spawPos = targetPos + Vector3.Transform( Vector3:Forward(), Quaternion:FromAxisAngle( Vector3:Up(), angle ) ) * self.spawnDist
   
  self.gameObject.transform:SetPosition( self.spawPos )

end

function Behavior:Update()

  --movement
  local targetPos = self.target.transform:GetPosition()

  self.gameObject.transform:LookAt( targetPos )
  self.gameObject.transform:MoveOriented( Vector3:Forward()*self.speed )
  
  --check for collisions with player
  local direction = targetPos - self.gameObject.transform:GetPosition()
       
  local distance = direction:Length()
  if distance < 1 then
       self.target:SendMessage( "Damage",self.power)
       CraftStudio.Destroy( self.gameObject ) 
  end

end

--Method called when a bullet collide 
function Behavior:Damage( )
   self.target:SendMessage( "AddPoint",self.ScoreWhenKill)
   CraftStudio.Destroy( self.gameObject )
end