Getting started part 6: Dragon Eggs - Getting serious about AI

If you didn't complete tutorial 5 but still want to play along, grab this: DragonEggs_Tutorial6Start.zip

This continues the game we started in part 3 and continued in part 4 and part 5.

So far the player is using the default movement speeds, let's tweak it a bit.

Let's speed him up. Modify your player.lua's OnPostInit() function as follows: (the changed parts are in bold text)

function OnPostInit() //run after being placed on a map
	this:SetDesiredSpeed(12);
	this:SetMaxMovementSpeed(12);
	this:SetAcceleration(1);

	LogMsg("Hi World! My name is " .. this:GetName() .. " and I like long romantic walks on the beach");
	... snipped ..
            

After using SetDesiredSpeed, it's a good idea to set the SetMaxMovementSpeed to make sure it's possible to move at the desired speed. SetAcceleration gives us more 'oomph' when we start moving. This feels less sluggish because we can reach the top speed quicker.

Giving the Dragon eyes

The last thing we did on the dragon was give him a basic script that had him walk to each checkpoint. Let's get a bit more advanced now. Here is what we want:

Ok. We've got a lot to do so let's get started. First, replace the function AddPatrolGoals with this one:

function AddPatrolGoals()
	
	local pointName = "point" .. random(1, 4); //a random number between 1 and 4. 
	
	this:GetGoalManager():AddApproach(GetEntityByName(pointName):GetID(), C_DISTANCE_CLOSE);
	this:GetGoalManager():AddRunScriptString("AddPatrolGoals()");
	
end
            

Now the dragon is less predictable and will run around the map forever because after hitting a check point, it will run AddPatrolGoals() again which will choose a new checkpoint.

Next, add a new function called OnMapInsert.

function OnMapInsert() //run as the entity is placed on an actual map, but before OnPostInit()
	GetWatchManager:Add(this, C_TIME_FOREVER); //always function offscreen
end
            
Now the dragon will start 'thinking' and be active as soon as the map loads and will function offscreen.

Tip: If a function called OnMapInsert exists, it will be run when the entity is placed on the map. As opposed to OnPostInit() which isn't run the entity comes onscreen, generally.

Normally an entity like the dragon doesn't operate when it isn't visible. This allows us to have very large levels without slow-down.

However, in some cases you want an Entity to move no matter where he is. The WatchManager lets us specify that the Dragon is ALWAYS "on", no matter where he is in relation to what the camera is currently showing on screen.

The WatchManager can also accommodate tricky things like "let him move around for 10 seconds after he's off the screen" and such to avoid the player from thinking "hey, everything freezes when I'm not there, dumb! ".

Now for seeing the player and chasing him, modify the OnPostInit() and add the new functions LookAround() and SetupPlayerChase() as follows.

function OnPostInit() //run during the entity's first logic update

	this:GetBrainManager():Add("StandardBase","");
	
	AddPatrolGoals();
	
	LookAround();
	this:SetTurnSpeed(6); //turn instantyl
	this:SetAcceleration(1); //speed up faster
end




function LookAround()
	
	//we already chasing him?
	if (this:GetGoalManager():IsGoalActiveByName("ChasingPlayer")) then
		//we're already chasing him.  Should we stop?
	
		//if we're far away AND can't see him, yes, stop
	
		if (this:GetDistanceFromEntityByID(g_playerID) > 400
			and not this:HasLineOfSightToPosition(GetPlayer:GetPos(), true) ) then
		
			//he's not visible and more than 400 pixels away.  Stop.
			GetTextManager:Add("(panting) Where did he go?", this);
			this:SetDesiredSpeed(2.4); //walk speed again

			//cancel the chase, start patrolling
			this:GetGoalManager():RemoveAllSubgoals(); //stop whatever we were doing
			AddPatrolGoals();
			this:GetGoalManager():PushDelay(2000); //wait two seconds first
		
		else
		
			//the player is still in range.  Let's recalcuate our chase path.
			SetupPlayerChase();
		end
	
	else
	
		//not currently chasing player.  But should we?
		//is the player close?
		if (GetPlayer != nil and this:GetDistanceFromEntityByID(g_playerID) < 700
			and this:HasLineOfSightToPosition(GetPlayer:GetPos(), true)) then
				//We can see the player!
				this:SetDesiredSpeed(5); //start running
				SetupPlayerChase();
				GetTextManager:Add("I see you!", this);
			end

	end
		
	//call this function again, randomly chooses a time to look again
	Schedule(random(500,1000), this:GetID(), "LookAround()");

end

function SetupPlayerChase()

	//cancel other goals and start chasing the player.  
	
	this:GetGoalManager():RemoveAllSubgoals(); //stop whatever we were doing
	local goal = this:GetGoalManager():AddNewGoal("ChasingPlayer");
	goal:AddApproachAndSay("(breaths fire)", g_playerID, C_DISTANCE_INSIDE);
			
end

            

Ok, LookAround() was a bit of a monster function. Let me explain the key ideas here.

1. After looking around, it schedules LookAround() (itself) to be called again, this will happen throughout the dragon's life, regardless of what its GoalManager is doing. Schedule() is very useful to do things at regular intervals.

2. We schedule with a slightly random time, this is good practice, because if we have 20 dragons we don't want them all running this function at the exact time. The randomness staggers it out, making game play smoother for CPU intensive things like the HasLineOfSightToPosition check.

3. We use the globals g_playerID and GetPlayer, which returns the players ID and the player Entity object respectively. We could have also used GetEntityByName("Player") but using the global is a little faster. How did it know who the player is? That's what AssignPlayerToCameraIfNeeded(this); does in our player.lua.

Tip: A game does not NEED an official "player" entity, but there are several handy things we can do if we have one, as we'll see when we setup the dragon/player collision a bit later.

4. When we see the player, we clear all goals and add some new ones under a subgoal called "ChasingPlayer". The reason I did that was so I could easily check for that goal name later, to see if we just found the player or I was already chasing him.

5. I added the SetTurnSpeed call so he won't appear to "slow down" at corners. If we were using a visual profile with 'directions', it would look cool to turn, but for now it just looks weird because the same sprite is used for all directions.

6. We're assuming the dragon can see in all directions at once, because we only have the one sprite. However, we could easily limit his vision with a check to IsFacingTarget if we wanted to.

Now, to really see what is going on, enable Display->Show Entity Goal and AI Data and watch how the tree of goals appear and disappear as the script kills and adds them.

Time 2 Die

This isn't really a game until the dragon can actually kill the player, right?

Let's do that now by setting up a collision callback so when the dragon touches the player, he will call a function called OnDamage() on the player if it exists. Then, the player can handle however he wants to die himself. (Instead of trying to do it all from the dragon's script)

First, we'll let the dragon know we want to know in a script callback when collisions with the 'player' Entity occur.

Change the dragon.lua's OnPostInit() as follows:

function OnPostInit() //run during the entity's first logic update

	this:GetBrainManager():Add("StandardBase","");
	
	AddPatrolGoals();
	
	LookAround();
	this:SetTurnSpeed(6); //turn instantly
	this:SetCollisionListenCategory(C_CATEGORY_PLAYER, true);
end
            

Now we will receive information when any entity that is marked with the "C_CATEGORY_PLAYER" flag is touched. But we'll get an error unless we add a function called OnCollision to the dragon.lua script. Do so as follows:

function OnCollision(vPosition, vVelocity, vNormal, depth, materialID, entity, state)

	 LogMsg("Hit the player!");
	 entity:RunFunction("OnDamage", vNormal, depth, nil, 5, 0, this); //let the player know we hit him and extra junk he may care about.  
end
            

If you play the game now and the dragon touches the player, nothing will happen! What's wrong? Well, we forgot to mark the player with the player flag. Add these two new lines:

function OnPostInit() //run during the entity's first logic update


	this:SetCategories(C_CATEGORIES_NONE);
	this:SetCategory(C_CATEGORY_PLAYER, true);

	this:SetDesiredSpeed(12);
	this:SetMaxMovementSpeed(12);
	this:SetAcceleration(1);
...more junk...

            
Now, if you play the game and the dragon attacks the player, you'll see that the Dragon's OnCollision() call we added gets notified, and an error about the player.lua not having an OnDamage() is displayed. Let's add that now to the player.lua.
//entParent is whoever sent the damage, entObject is the actual damaging object/projectile if not nil
function OnDamage(normal, depth, entParent, strength, extra, entObject)
	
	
	if (this:VariableExists("m_isDead")) then
		//we're already dead.  ignore this
		return;
	end
	
	m_isDead = true; //create a variable so we know we're dead.
	
	//set it so we can't be controlled by input anymore
	GetInputManager:RemoveBindingsByEntity(this);
	
	//stop moving, in case we were
	ResetKeys();
	
	//flash red for a long time
	this:GetBrainManager():Add("ColorFlash", "pulse_rate=50;g=-200;b=-200;remove_brain_by_pulses=1000");

	this:GetGoalManager():AddSay("Aaaarrrrgggghhh!", C_FACING_NONE);
	this:GetGoalManager():AddSay("I guess the game is over.  Press Ctrl-Shift-R to restart or reinit " ..
		"the player by hitting F1 and then dragging him a few pixels.", C_FACING_NONE);
	
end
            

Now if you play, the dragon can actually hurt you. We're making some progress now. The dragon is a little too easy to dodge. No problem, cut and paste three more dragons onto the map and have them start in strategic postions. If you find they are getting stuck somewhere, add more path-nodes around that area, the dragon will use them to calculate alternate routes in conjested areas.

The heart of the game - The dragon eggs

When a player touches an egg, this is what should happen:

Convert your egg tile you made earlier into an entity and set its script to "egg.lua" and click edit. (Note, this may change its visual alignment to 'centered', you can use the visual alignment editor to turn it back to "up left" so the collision we made earlier still fits right)

'Cut and paste this in for the egg.lua script:

function OnInit() //run upon initialization
	this:SetIsCreature(true); //hint to engine to ignore us when calculating pathfinding
end


function OnMapInsert()

	//count how many eggs have been created
	
	if (this:VariableExists("g_eggCount")) then
		//global already exists.  Let's use it.
		_G.g_eggCount = g_eggCount + 1;
	else
		//no global exists yet, let's create it so all eggs can use it
		_G.g_eggCount = 1;  //the _G. is the only way to set a global
	end
end

function OnPostInit() //run during the entity's first logic update

	//let's not actually 'collide' with anything, otherwise the dragons could push us around
	this:SetCollisionCategories(C_CATEGORIES_NONE); //no collision at all

	//register to get OnCollision calls when the player walks though us.
	this:SetCollisionListenCategory(C_CATEGORY_PLAYER, true);
	
end

function OnKill() //run when removed

	RemoveEggFromGlobalCount();

end

function RemoveEggFromGlobalCount()

	if (this:VariableExists("m_removedEgg")) then
		//already removed it.
		return;
	end
	
	_G.g_eggCount = g_eggCount - 1;
	
	m_removedEgg = true;
end

function OnCollision(vPosition, vVelocity, vNormal, depth, materialID, entity, state)
	//if this is called, we must have hit the player, which will be passed as the "entity" var.

	//first, turn off our collision 'listening' so this won't get called again
	this:SetCollisionListenCategory(C_CATEGORY_PLAYER, false); //don't be notified anymore
     
	//sound effect
	local soundID = this:PlaySound("audio/gui/change_selection1.ogg");
	GetSoundManager:SetSpeedFactor(soundID, 0.5); //slow down the sound
	
	//scale down to nothing and then delete itself
	this:GetBrainManager():Add("Scale", "scalex=0.1;scaley=0.1;scale_speed_ms=500;delete_entity=true");

	//let the player know where he stands
	
	RemoveEggFromGlobalCount();
		
	if (g_eggCount > 0) then
		//still more eggs after this
		GetTextManager:Add("(egg collected, " .. g_eggCount .. " left)", entity);
	else
		//this is the last egg!
		GetTextManager:Add("You collected the last egg, Huzzah!  Uh.  You win?", entity);
	end

end
            

This looks a bit intimidating so let me break it down a bit.

1. We used OnMapInsert() again to get the script to do something right as it is added to the map. This is a perfect way to add 1 to a global variable so we can keep track of how many eggs exist.

2. If g_eggCount doesn't exist, we create it as a global by putting "_G. " in front of it when we assign something to it. If we didn't do that, a vairable called g_eggCount would be created in our LOCAL namespace per egg, which is not what we want. It's a lua thing, don't fight it.

3. We used VariableExists() to check for the global, because otherwise we'd get a "you tried to read from an unitted var" error if we tried to test it for nil or something.

4. The RemoveEggFromGlobalCount() function is designed so even if we call it more than once, it will only remove 1 from the global g_eggCount time. That's because I run it in the OnCollision, but I also run it in the OnKill(). Because what if an egg is deleted in the editor? We would still want g_eggCount decremented.

Fine tuning

Scatter the eggs around the level (make sure you're editing the "master" files), place everything just so and then restart and click "New" to run as the player.


Don't forget to delete your original starting egg, otherwise the level will be impossible to finish because the player can't reach that egg!

How does it play? Well, my dragons were getting stuck against eachother in the hallways, so I added some more path-nodes.

Here is what I have now:

If you want to play my version, here it is.

It's now playable! But wouldn't it be nice to have a player that is actually animated? Let's see what we can do about that...

Tip: Remember to restart and click new when playing your game. Don't play in the editor! If you do, your eggs won't come back and the dragons won't start in the same place every time.

Continue to the next part

Back to tutorial index


© Copyright Seth A. Robinson and Robinson Technologies