Saturday, January 26, 2013

Come and Go: Implementing 'Enter' and 'Leave' scripting events

So, I've had a collision scripting event set up for a while. One of the limitations of collision, however, is that it will fire every single frame while the two entities in question are overlapping. What if I wanted to prevent that from happening?

Well, one hack-ish way to accomplish this is to call the following method from inside the method:

e.Script:Destroy()

But this isn't really what we wanted. What I want is for an event to fire when the collision begins--but if the collision ends, and then begins again, I want that event to fire yet again. Destroying the script entirely is no good.

Introducing the 'Enter' script event!



'Enter' script events fire whenever an entity begins contact with another entity. If you leave and come back again, the event fires again. Imagine a switch that you step on that 'untoggles' when you step off of it, and toggles when you step back on--such a situation could be implemented with the Enter script.

Of course, what would 'Enter' be without 'Leave'? And what would a blog post here be without a video to show you all of this stuff? Four major things to notice in the video:

  • Most of the art is no longer ripped from A Link to the Past! In fact, it's not ripped from anywhere! I'll save a special future blog post to introduce the artist, but the game's now one major step closer to being something beyond an engine.
  • The second part of the video demonstrates that the player is an entity just like everything else. Any cool entity effects from earlier blog posts can just as easily be applied to the player!
  • You can't see the code in the video, but I added a method to the scripting interface which allows the designer to find an entity by a special identifier (tag). This is how the switches in the video know how to spawn balls from the braziers.
  • Texture filtering is on now, everything looks much smoother. There's a few quirks to work out with tiling textures, but they're not really critical at the moment.
Enjoy!




Tuesday, January 22, 2013

No More Player

Okay, this isn't the flashiest update. I spent most of the weekend making major changes to the internal infrastructure of the engine. The most major of these changes was removing the Player class which was previously used to represent the state, position, and current animation of...well, the player.

All of these features and more are supported by the Entity system, so I converted all instances of player logic to instead use an entity. The end result was the deletion of a bunch of code that wasn't needed anymore because the features were already supported by the Entity system.

I also changed a bunch of stuff with how the scripting infrastructure is organized and edited through the level editor, but it's not worth showing yet.

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))))


Monday, January 14, 2013

Advanced Animation Properties, Batch Frame Importer

I've been working on the Entity Property Controller sub-system, which will be one of the most interesting and critical features of the engine.

Now, I'm not really ready to demonstrate that system yet, since it's not done yet. But to briefly explain, a property controller is something that is attached to an entity to control one its properties--position, size, color, animation frame, etc. I don't want to go in too much detail since that's for the next post, but I did want to show how one of these controllers is used to control frame animation in a more flexible way.

Here's a video showing me working with the Asset Editor and Level Editor to import and configure a 36-frame animation (which has no business being in this game, it's just temp art). Check it out and stay tuned for some really cool stuff!





Wednesday, January 9, 2013

Refactoring Textures and Animations

(Disregard what I said I'd be working on last, more advanced layering. I changed my mind.)

Code-design update here mostly, so if you aren't interested in programming or anything this might be boring.

My entity model was designed such that you could set either a texture (via a TextureID property) or an animation (via an AnimationID property). This worked, but was a mediocre design since it resulted in multiple occurrences of code that looked like this:


            if (entity.TextureID != 0)
            {
                //Do something here
            }
            else if (entity.AnimationID != 0)
            {
                //Do another thing here
            }


The missing abstraction here is that a Texture is simply an animation with one frame. So, I changed the data model so that all textures have a frame collection, with the normal case being a single frame, and then eliminated the Animation structure completely. Most of the work here was writing a migration utility which updated all existing data to fit this model, and then updating the Asset Editor program to handle these new structures. Here's what the texture editor portion of it looks like now--now there's two tabs on the texture editor, one for editing individual texture frames and one for previewing the animation.



Anyway, yeah, not the most exciting update in the world, but I'm trying to keep a fairly clean design and get these things right before actually creating levels, since I'm still at the stage that I can make major design changes with minimal impact.

Saturday, January 5, 2013

Palettes and Fade Transitions

I added a sorely-needed feature to the level editor--palettes! So, this update won't be quite as glamorous as previous ones because tool development isn't always the most exciting thing. But I did throw something cool in near the end, so read on...

Previously when using the level editor, although my copy-and-paste (or copy-drag) functionality is pretty useful, selecting textures was not efficient at all. Navigating a hierarchical list of textures to find the right one works alright, but level designers are human and work much better with a visual palette of textures.

So, I added a palette designer to the asset editor tool which allows us to create Palette Data, which is consumed solely by the level editor. Here's what it looks like:


Currently the palette editor allows you to drop textures and animations in any of the 400 (20x20) slots, for easy access in the level editor. You can also rotate/flip tiles ahead of time--the tiles you see here for the right side of the rock wall are actually redundant, but it makes for a quicker experience when designing levels.

Here's what it looks like in-editor:


Adding this palette control also necessitated the concept of a 'current layer'--so you can see the layer picker on the right. The long bar is the 'current layer' selector, and determines what layer the palette tile you click is dropped onto. The small boxes in the layer selector represent visibility.

I also added a few more things not shown here, mostly keyboard shortcuts (arrow key navigate from tile to tile, to make using the palette quicker), M shows and hides models, etc.

Okay, now for the slightly cooler stuff. Remember the scrolling section transition from last time? Well, now there's another type: a fade-in-fade-out transition. Here's a video of it in action.




Things to note here:
  • The color of the fade is configurable--you can see black and white fades.
  • The animation of the player and the direction the player moves on the 'fade in' (latter) part of the transition is configurable. When going from cave to cave, for example, we want to set the incoming direction to 'down' and the incoming animation to whatever player animation makes sense--which is 'player_walking_down' in this case. But the cave on the left (inexplicably) leads the player to another level, outside, moving to the right, so that uses 'player_walking_right'.
Anyway, next up I think I'll be adding a better-defined layering scheme, geared towards allowing multiple playable layers (think: overpasses, underpasses, stairs, ladders) in a single section.