Thursday, January 17, 2013

Blowing Up Balls: Setting Entity Controllers


I've finished the first implementation of the Entity Control System mentioned in last week. Since this is all about animating objects (and I don't just mean controlling an animation frame), a video is really the only way to convey what was done here. Here's some of the features you'll see in the video:

  • The ability to control the properties Position, Size, Rotation, Opacity, Color, and Animation Frame through a variety of mechanisms.
  • Setting up an arbitrary number of controllers to change these properties in sequence or in parallel
  • Inserting arbitrary functions to run in between any of these steps.
  • Having these functions spawn more entities with their own independently running controllers.

Check it out! And then come back here and check the scripts out!


I hope you enjoyed the demo. If you're into coding, below are all of the scripts attached to the playerDamage event of each of the twelve braziers, to give you an idea of how these were implemented.

Note: before this lengthy list, I'd like to point out that, like all programming, abstraction and reuse is critical. That's why my a soon-to-come feature will introduce the Scripting Library where scripts can be written and reused by any entity in the game (where appropriate). Up until now, scripts have been directly attached to events on entities, but this will soon be augmented by an 'external script reference' concept which allows the level designer to create and reuse common scripts.

Anyway, here's the Lua scripts for the video. A few explanations of the code:
  • The variable e represents the event arguments. In each of these examples, the event is tied to the 'playerDamageSelf' event on the braziers, so e.Entity represents the brazier itself.
  • The helper funcion v2 is shorthand for Vector2(x,y)
  • The Animate method is just shorthand for adding a frame controller that oscillates between 0 and 1 using the default speed/looping mode set up in the assets manager.
  • The functions sequence, range, motion, interp, delay, and action are all helper methods which create the correct parameters to pass into entity:Control()
  • sequence accepts a list of controllers as arguments, and returns a controller itself
  • range accepts a looping mode ('finish' means the controller is finished at the end of the range), a duration, a list of interpolators, and returns a controller
  • interp creates an interpolator for use with range, using accepts a property (frame, opacity, etc.), a start and end values, and a curve interpolator
  • delay creates a controller which does nothing but wait a given amount of time
  • action takes in a Lua function and returns a controller which does nothing but execute that action once before completing.

1. Creating a static entity


local self = e.Entity;

local t = { texture = "sphere", x = self.Position.X, y = self.Position.Y + 32 * 3, w = 32, h = 32 }

local sphere = e.Section:CreateEntity(t)


2. Applying a zigzag style animation.



local self = e.Entity;

local t = { texture = "sphere", x = self.Position.X, y = self.Position.Y + 32 * 3, w = 32, h = 32 }

local sphere = e.Section:CreateEntity(t)
sphere:Animate()



3. Interpolating the size property between two values with a certain duration.



local self = e.Entity;

local t = { texture = "sphere", x = self.Position.X, y = self.Position.Y + 32 * 3, w = 32, h = 32 }

local sphere = e.Section:CreateEntity(t)
sphere:Animate()

sphere:Control(
  sequence(
    range("finish", 1, interp("size", v2(32,32), v2(64,64), "linear"))))



4. Interpolating both opacity and size in parallel.



local self = e.Entity;

local t = { texture = "sphere", x = self.Position.X, y = self.Position.Y - 32 * 3, w = 32, h = 32 }

local sphere = e.Section:CreateEntity(t)
sphere:Animate()
sphere.Opacity = 0;

sphere:Control(
  range("finish", 1, 
    interp("size", v2(32,32), v2(64,64), "easeIn"),
    interp("opacity", 0, 1, "sin")))




5. Executing a sequence of #4 followed by three motion ranges, each with different easing functions.



local self = e.Entity;

local t = { texture = "sphere", x = self.Position.X, y = self.Position.Y - 32 * 3, w = 32, h = 32 }

local sphere = e.Section:CreateEntity(t)
sphere:Animate()
sphere.Opacity = 0;

local p1 = v2(sphere.Position.X, sphere.Position.Y);
local p2 = v2(sphere.Position.X + 50, sphere.Position.Y - 50);
local p3 = v2(sphere.Position.X + 50, sphere.Position.Y + 50);

sphere:Control(
  sequence(
    range("finish", 1, 
      interp("size", v2(32,32), v2(64,64), "linear"),
      interp("opacity", 0, 1, "sin")),
    range("finish", 2,
      interp("position", p1, p2, "easeOut")),
    range("finish", 1,
      interp("position", p2, p3, "easeIn")),
    range("finish", 1,
      interp("position", p3, p1, "linear"))
))



6. Appending a forever-looping controller that cycles the 'green' property between 0 and 1, running in parallel with #5. Also, I set the model to 'circle' here so the player collides with it.



local self = e.Entity;

local t = { texture = "sphere", x = self.Position.X, y = self.Position.Y - 32 * 3, w = 32, h = 32 }

local sphere = e.Section:CreateEntity(t)
sphere:Animate()
sphere.Opacity = 0;

local p1 = v2(sphere.Position.X, sphere.Position.Y);
local p2 = v2(sphere.Position.X + 50, sphere.Position.Y - 50);
local p3 = v2(sphere.Position.X + 50, sphere.Position.Y + 50);
sphere:SetModel("Circle")
sphere:AddCollisionCategory("wall")
sphere:Control(
  sequence(
    range("finish", 1, 
      interp("size", v2(32,32), v2(64,64), "linear"),
      interp("opacity", 0, 1, "sin")),
    range("finish", 2,
      interp("position", p1, p2, "easeOut")),
    range("finish", 1,
      interp("position", p2, p3, "easeIn")),
    range("finish", 1,
      interp("position", p3, p1, "linear"))
))


sphere:Control(
  range("zigzag", .5, interp("g", 1, 0, "linear")))



7. Fade the ball in and set a non-interpolated motion controller with a specific velocity and acceleration.



local self = e.Entity;

local t = { texture = "sphere", x = self.Position.X, y = self.Position.Y + 32 * 3, w = 32, h = 32 }

local sphere = e.Section:CreateEntity(t)
sphere:Animate()
sphere.Opacity = 0;

sphere:SetModel("Circle")
sphere:AddCollisionCategory("wall")
sphere:Control(
  sequence(
    range("finish", 1, 
      interp("size", v2(32,32), v2(64,64), "linear"),
      interp("opacity", 0, 1, "sin")),
  motion(v2(100, 100), v2(0, -60))))



8. Demonstrating the ability to run arbitrary scripts as part of the sequence--this is just like #7, except the collision with the player isn't activated until the ball is fully faded in.



local self = e.Entity;

local t = { texture = "sphere", x = self.Position.X, y = self.Position.Y - 32 * 3, w = 32, h = 32 }

local sphere = e.Section:CreateEntity(t)
sphere:Animate()
sphere.Opacity = 0;

sphere:SetModel("Circle")

sphere:Control(
  sequence(
    range("finish", 3, 
      interp("size", v2(32,32), v2(64,64), "linear"),
      interp("opacity", 0, 1, "sin")),
  action(function() sphere:AddCollisionCategory("wall") end),
  motion(v2(60, -100), v2(0, 60))))



9. Like #8, except after the ball is done fading in, its damage event is attached, which destroys it.



local self = e.Entity;

local t = { texture = "sphere", x = self.Position.X, y = self.Position.Y - 32 * 3, w = 32, h = 32 }

local sphere = e.Section:CreateEntity(t)
sphere:Animate()
sphere.Opacity = 0;

local explode = function()
sphere:Destroy()
end

local readyToExplode = function ()
sphere:AddCollisionCategory("wall")
sphere:Attach("playerDamageSelf", explode)
end

sphere:SetModel("Circle")
sphere:Control(
  sequence(
    range("finish", 3, 
      interp("size", v2(32,32), v2(64,64), "linear"),
      interp("opacity", 0, 1, "sin")),
  action(readyToExplode),
  motion(v2(60, -100), v2(0, 60))))



10. The destruction of the ball also spawns an explosion.



local self = e.Entity;

local t = { texture = "sphere", x = self.Position.X, y = self.Position.Y - 32 * 3, w = 32, h = 32 }

local sphere = e.Section:CreateEntity(t)
sphere:Animate()
sphere.Opacity = 0;

local explode = function(swordEvent)
  local t = { texture = "explosion", x = sphere.Position.X, y =                           sphere.Position.Y, w = 64, h = 64 }
  local explosion = e.Section:CreateEntity(t)
  explosion:Control(
    sequence(
      range("finish", 1, interp("frame", 0, 1, "linear")),
      action(function() explosion:Destroy() end)))

  sphere:Destroy()
end

local readyToExplode = function ()
  sphere:AddCollisionCategory("wall")
  sphere:Attach("playerDamageSelf", explode)
end

sphere:SetModel("Circle")
sphere:Control(
  sequence(
    range("finish", 3, 
      interp("size", v2(32,32), v2(64,64), "linear"),
      interp("opacity", 0, 1, "sin")),
  action(readyToExplode),
  motion(v2(-60, -100), v2(0, 60))))



11. The destruction of the ball additionally spawns 30 pieces of debris with random positions, rotations, and initial frames. They are given motion controllers to make them fall down.



local self = e.Entity;

local t = { texture = "sphere", x = self.Position.X, y = self.Position.Y + 32 * 3, w = 32, h = 32 }

local sphere = e.Section:CreateEntity(t)
sphere:Animate()
local spawnExplosion = function(x, y, size, vx, vy)
  local t = { texture = "explosion", x = x, y = y, w = size, h = size }
  local explosion = e.Section:CreateEntity(t)
  explosion:Control(
    sequence(
      range("finish", .5, interp("frame", 0, 1, "linear")),
      action(function() explosion:Destroy() end)))
  explosion:Control(motion(v2(vx,vy)))
end

local spawnDebris = function(x, y)
  local newX = -16 + math.random(32)
  local newY = -16 + math.random(32)
  local vx = -100 + math.random(200)
  local vy = 50 + math.random(220)
  local t = { texture = "spheredebris", x = x + newX, y = y + newY, w = 16, h = 16 }
  local debris = e.Section:CreateEntity(t);
  debris.Frame = math.random()
  debris.Rotation = math.random(2 * math.pi)
  debris:Animate()
  debris:Control(motion(v2(vx,vy), v2(0, -200)))
end

local explode = function(swordEvent)
  spawnExplosion(sphere.Position.X, sphere.Position.Y, 64, 0, 0);
  for i = 1, 30, 1 do
    spawnDebris(sphere.Position.X, sphere.Position.Y)
  end
  sphere:Destroy()
end

local readyToExplode = function ()
  sphere:AddCollisionCategory("wall")
  sphere:Attach("playerDamageSelf", explode)
end

sphere:SetModel("Circle")
sphere:Control(
  sequence(
    range("finish", 1, 
      interp("size", v2(32,32), v2(64,64), "linear")),
  action(readyToExplode),
  motion(v2(-60, 100), v2(0, -60))))




12. The debris pieces are set to randomly blow themselves up after a variable time, spawning smaller explosions.


local self = e.Entity;

local t = { texture = "sphere", x = self.Position.X, y = self.Position.Y + 32 * 3, w = 32, h = 32 }

local sphere = e.Section:CreateEntity(t)
sphere:Animate()
local spawnExplosion = function(x, y, size, vx, vy)
  local t = { texture = "explosion", x = x, y = y, w = size, h = size }
  local explosion = e.Section:CreateEntity(t)
  explosion:Control(
    sequence(
      range("finish", .5, interp("frame", 0, 1, "linear")),
      action(function() explosion:Destroy() end)))
  explosion:Control(motion(v2(vx,vy)))
end

local spawnDebris = function(x, y)
  local newX = -16 + math.random(32)
  local newY = -16 + math.random(32)
  local vx = -100 + math.random(200)
  local vy = 50 + math.random(220)
  local t = { texture = "spheredebris", x = x + newX, y = y + newY, w = 16, h = 16 }
  local debris = e.Section:CreateEntity(t);
  debris.Frame = math.random()
  debris.Rotation = math.random(2 * math.pi)
  debris:Animate()
  debris:Control(motion(v2(vx,vy), v2(0, -200)))
  debris:Control(
    sequence(
      delay(.5 + math.random()),
      action(function() spawnExplosion(debris.Position.X, debris.Position.Y, 32, debris.Velocity.X, debris.Velocity.Y) debris:Destroy() end)))
end

local explode = function(swordEvent)
  spawnExplosion(sphere.Position.X, sphere.Position.Y, 64, 0, 0);
  for i = 1, 30, 1 do
    spawnDebris(sphere.Position.X, sphere.Position.Y)
  end
  sphere:Destroy()
end

local readyToExplode = function ()
  sphere:AddCollisionCategory("wall")
  sphere:Attach("playerDamageSelf", explode)
end

sphere:SetModel("Circle")
sphere:Control(
  sequence(
    range("finish", 1, 
      interp("size", v2(32,32), v2(64,64), "linear")),
  action(readyToExplode),
  motion(v2(60, 100), v2(0, -60))))


No comments:

Post a Comment