Friday, March 29, 2013

Those spike thingies: Entity state switching

An action-adventure game isn't really much fun without enemies, so I thought it might be fun to implement a basic enemy next. I ended up with these little guys:


They're pretty simple enemies. If you recognize any of these from the Zelda series, you probably already know what they do:


These spike things (which the Internet actually tells me are called 'blade traps') lie waiting until the player crosses their horizontal or vertical axis, at which point they charge at the player. When they hit another spike, or a wall, they return to their original location. I thought this would be a simple enough starting point for implementing a very basic AI control mechanism.

I actually added a twist to mine--the player has half a second to get out of the way upon 'waking' the spike enemy before it charges at them.

The spike, therefore (at least, my implementation of it) has four states:

Standard - The spike's default state. It's at rest, waiting for the player to approach it
Waking - The player has woken the spike and has 0.5 seconds to get out of the way. Depending on this, the spike will either return to 'Standard', or move to...
Chasing - The spike moves in a straight horizontal or vertical line in the appropriate direction.
Returning - The spike goes back to its original position, and then returns to the Standard state.

This is probably the best place for a video, which shows the thing in action:


So, what did I have to do to get this working? A few things. As you might know from previous updates, entities function by having scripts (which could also be described as behaviors) attached to them. The following scripts are currently supported by the infrastructure:

Init - Called immediately when the entity is loaded
Update - Called every n to m seconds, depending on how the designer specifies. An update could be called every .5 seconds, every .03 seconds, or randomly every 1 to 4 seconds.
Collide - Occurs when the entity collides with another one. A previous post describes setting up collision categories for the collide event.
Enter - Occurs when the entity collides with another one, but doesn't occur again until they have 'uncollided' and collided again.
Leave - The complement event to 'enter'.
Trigger - This is a custom behavior which the engine doesn't call directly -- it is meant to be called by other scripts. For example, an enemy might call  a custom 'damage' trigger (with custom arguments--say, the damage type and the strength) on another entity.

So, that's a quick recap of the scripting system I've set up. It's fairly simple, but allows for infinite possibilities.

I decided I'd implement the Entity State system by introducing an Entity State Collection, which consists of: 
  • The current state
  • A collection of states.
An Entity State consists of:
  • The name of the state (i.e. 'waking', 'chasing', etc.)
  • A collection of script behaviors--collide, update, init, etc. Init is called when the state is changed.
So now, I can cleanly isolate scripting behaviors to certain states. In addition, entities still have a 'global' implied state; that is, script behaviors that are always running no matter what state the entity is in. For example, the player might leave footsteps behind (implemented by an Update script that runs every half second) whether he/she is walking, running, being knocked back, etc. This behavior would therefore be put into the global behavior collection, and not in a particular state.

So there you have it. One thing I don't have (yet) is an Entity Template Designer, so in the meantime I've temporarily hacked the engine to set up the spike enemy with the appropriate behaviors. Here is what the code looks like; as you can see, it's fairly straightforward:


Eventually, a designer tool of some sort will replace this code. The 'table' I'm indexing is from a Lua script file. That is all for this update. The actual script can be found below. Thanks for reading!


local t = { };
------------------------------------
t["init"] = function(e)

--Global initialization. Set the original position of the spike as two custom state variables
local entity = e.Entity
entity["returnX"] = entity.Position.X
entity["returnY"] = entity.Position.Y

end
------------------------------------

t["standard_init"] = function(e)

e.Entity:SetTexture("spike_sleep");

end
------------------------------------
t["standard_update"] = function(e)

local section = e.Section;
local entity = e.Entity;
local position = entity.Position;

local entity = e.Entity;

--Create two large rectangles used to test for collision against the player
local hpoly = polySquare(v2(position.X, position.Y), v2(1000, 1));
local vpoly = polySquare(v2(position.X, position.Y), v2(1, 1000));

--Actually run the collision, colliding against tag 'player'
local h = e.Section:PolyEntitiesTag(hpoly, "player");

--Check the horizontal (and vertical) collision results and set the state to waking if either hit
if (h.Count > 0) then

entity:SetState("waking");

else

local v = e.Section:PolyEntitiesTag(vpoly, "player");

if (v.Count > 0) then

entity:SetState("waking");

end
end

end
------------------------------------
t["waking_init"] = function(e)

local entity = e.Entity
--Set up function that will be run in .5 seconds

local stillAttack = function()

--Perform a similar collision detection as in the 'standard' state
local position = entity.Position;
local hpoly = polySquare(v2(position.X, position.Y), v2(1000, 1));
local vpoly = polySquare(v2(position.X, position.Y), v2(1, 1000));
local h = e.Section:PolyEntitiesTag(hpoly, "player");
local chase = false;
if (h.Count > 0) then
chase = true;
else

local v = e.Section:PolyEntitiesTag(vpoly, "player");
if (v.Count > 0) then
chase = true;
end
end

if chase then entity:SetState("chasing") else entity:SetState("standard") end

end

--Open the eye and then run the above function in .5 seconds
entity:SetTexture("spike_awake");
entity:ControlNamed(
 sequence(
delay(.5),
action(stillAttack)), "wakecheck");
end
------------------------------------
t["chasing_init"] = function(e)

--We've entered the chasing state, let's figure out which way to actually go
local entity = e.Entity;
local player = e.Section:FindEntity("player")
if player == null then return end

--Do some angle calculations to figure out whether to go up, down, left, or right
local playerPosition = player.Position;
local position = entity.Position;
local rawAngle = math.atan2(playerPosition.Y - position.Y, playerPosition.X - position.X)
local angle = closestCardinalAngle(rawAngle)
local speed = 300;

--Move in that direction
entity:ControlNamed(motion(v2(math.cos(angle) * speed, math.sin(angle) * speed)), "motion")

end
------------------------------------
t["chasing_collide"] = function(e)

--More interesting things will happen here someday
e.Entity:SetState("returning");

end
------------------------------------
t["returning_init"] = function(e)

--Go back to start
local entity = e.Entity;
local distance = math.abs(entity.Position.X - entity["returnX"]) +math.abs(entity.Position.Y - entity["returnY"]);
local time = distance / 100;
entity:ControlNamed(
sequence(
range("finish", time, interp("position", v2(entity.Position.X, entity.Position.Y), v2(entity["returnX"], entity["returnY"]), "linear")),
action(function() entity:SetState("standard") end)), "motion");
end
------------------------------------
return t;


Monday, March 18, 2013

Particles

Particles are cool! Lots of games and game engines have ways of setting up things that emit hundreds or thousands of little glowing particles. Technically I already had most of that in place already via scripting and the Section.CreateEntity method. One thing I didn't have, though, was the 'glowing' part, an effect achieved with Additive Blend mode (in contrast with regular old transparency).


See all the particles? A picture tells a few words. But it's not a thousand in this case. Luckily, this video does tell a thousand words (almost--make sure annotations are on for all of these!). Enjoy!


Next up I'll be working on Entity State. That is, I'll be setting up the infrastructure for allowing entities to be in different states, where each state has a collection of behaviors (scripts). This will be the framework for Enemy design, which should be exciting!

Friday, March 8, 2013

Entity Hierarchies

I've added Entity parenting to the engine!

A powerful component of some game engines--whether they're 2D or 3D--is the ability to give things (enemies, fireballs, particles, whatever) parent-child relationships such that transformations (moving, scaling, rotating) to the parent automatically affect all children. 

For example, an enemy might cast a spell that makes 4 fireballs swarm around him. Instead of the programmer manually calculating the position these fireballs--which can get quite complicated as the player moves around--a system might be set up so that the fireballs are position and moved with respect to the enemy. So, from a programming standpoint, the fireballs simply spin around (0, 0), and when the enemy moves, the fireballs move with it.

I have a video, as always, that demonstrates this new feature. Check it out!


Additional notes on the video:
  • You'll notice that I have a new character sprite. Say goodbye to Link and say hello to...well, it's a temporary design and will most likely change, so there's no point giving him a name.
  • Those test triggers (the white diamonds) appear to show off some sort of particle system. Although you might call them 'particles', they're actually just entities just like everything else, meaning they support the complete feature set thus far.
The entire scenario demonstrated in the video is entirely designed within the level editor via the scripting interface. For example, here's the part of the script that creates the tiny little balls at the end:

local section = e.Section;
local target = section:FindEntities("ball")
for ball in enum(target) do
  for i = 1, 8 do
    local angle = 2 * math.pi * i / 8
    local r = 32
    local x = math.cos(angle) * r
    local y = math.sin(angle) * r
    local t = { w = 8, h = 8, texture = "small_ball", 
                x = 0, y = 0 }
    local smallBall = section:CreateEntity(t)
    ball:AppendChild(smallBall)
    smallBall:Control(
     sequence(
       delay((i - 1) / 8),
         range("finish", .5, interp("position", v2(0,0), v2(x,y),                        "easeIn"))))
     smallBall:SetModel("Circle")
     smallBall:AddCollisionCategory("wall")
  end
end

Notes on the script:
  • The Section.FindEntities method returns all entities with a given tag. In a previous script, I had tagged all of the large balls with the tag 'ball'.
  • Entity.AppendChild is how parenting is set up
  • Note that the small balls are created with x = 0 and y = 0. This is because the small balls are positioned relative to their parents, not the world.