Log In  
2

Cart [#19329#] | Copy | Code | 2016-03-20 | Link
2

Sometimes, we want to be able to play animations in reverse. This is the advanced animation function with a reverse option. In this case, the foliage grows, stays for 5 seconds before shrinking away (playing the "grow" animation in reverse), disappears for 10 seconds, then repeats.

P#19328 2016-03-20 16:33

7

In the last post, we looked at some basic animation, collision and AI examples. In this one, we're going to take it a few steps further and improve upon these functions.

During the development of my game (tentatively titled "Castle of Thunder", which is a port of INTV's Thunder Castle), I realized that I was going to need some more advanced functionality, because the first enemy is a total of 4 sprites when walking horizontally, and 2 sprites when working vertically. Since we had all of our animation stepping, animation speed, frame numbers, etc stored as properties of our actor in our simple animation demo, it works great for 8x8 actors, but not as well when that actor is comprised of several sprites.

Further, I found that I needed to be able to specify x and y offsets for actors with multiple sprites, so that subsequent sprites can have custom positioning and aren't drawn on top of the first. I also needed a way to reset interrupted animations, so that when they start again, they don't start where they left off when they were interrupted. Finally, I needed a way to stop animations that aren't supposed to loop, as well as set a custom frame for when the animation is stopped, and set flipping for both x and y. That's a lot of added functionality, so I decided to rewrite the animation function from scratch, and instead of passing it a million parameters, we could pass it an object/table containing all of these options.

The object that we pass to the new animation function will be properties of the actor object. So basically, each animation will have its own table filled with all of the options for that animation. Here's an example:

e1={ -- enemy 1
  anim={
    play={}, -- currently playing
    walklrh={}, -- walk l/r head
    walklrb={}, -- walk l/r body
    walklrt={}, -- walk l/r tail
    walklrf={}, -- walk l/r fire
    walkuh={}, -- walk up head
    walkut={}, -- walk up tail
    walkdh={}, -- walk down head
    walkdt={}, -- walk down tail
    deathh={}, -- death head
    deathb={}, -- death body
    deatht={}  -- death tail
  }
}

e1a=e1.anim

-- sets up each anim with default options
-- we do this so eo don't have to set each one
-- individually above, to save lots of tokens
for k in pairs(e1a) do
  e1a[k].start=0 -- starting frame
  e1a[k].frames=4 -- number of frames in animation
  e1a[k].speed=7 -- animation speed
  e1a[k].flipx=false -- flip x
  e1a[k].flipy=false -- flip y
  e1a[k].loop=true -- loop animation?
  e1a[k].reset=false -- a frame, if you want to stop on a specific frame
  e1a[k].step=0 -- step counter, used to set anim speed
  e1a[k].current=0 -- current frame
end

-- set custom options where needed

-- walk left/right
e1a.walklrh.start=36 -- head
e1a.walklrb.start=40 -- body
e1a.walklrt.start=44 -- tail
e1a.walklrf.start=48 -- fire

-- walk up
e1a.walkuh.start=60 -- head
e1a.walkut.start=56 -- tail
e1a.walkut.flipy=true

-- walk down
e1a.walkdh.start=52 -- head
e1a.walkdt.start=56 -- tail

-- death
e1a.deathh.start=64 -- head
e1a.deathh.loop=false
e1a.deathb.start=68 -- body
e1a.deathb.loop=false
e1a.deatht.start=72 -- tail
e1a.deatht.loop=false

Of course, you can also change any of these options dynamically at any time throughout the game if certain conditions are met or whatever. For example, my "walklr" animations are the same for walking left or right, but my sprites are facing right. I just check if the enemy is walking left, and set "flipx" to true on the "walklr" animations, and false again if he's walking right. Similarly, if he's walking up, his tail is flipped on the y axis.

Here's the final animation function:

function anim(a,anim,offx,offy)
  local sta=anim.start
  local cur=anim.current
  local stp=anim.step
  local spd=anim.speed
  local flx=anim.flipx
  local fly=anim.flipy
  local lop=anim.loop

  anim.step+=1
  if(stp%flr(30/spd)==0)    cur+=1
  if(cur==anim.frames) then
    if(lop) then cur=0
    else cur-=1 end
  end

  anim.current=cur
  a.anim.play=anim

  if(not offx) offx=0
  if(not offy) offy=0
  -- draw the sprite
  spr(sta+cur,a.x+offx,a.y+offy,1,1,flx,fly)
end

It's not that much bigger than the simple version, but it's way more powerful. To call it for your huge enemy, you'd do something like this:

local an=e1.anim

anim(e1,an.walklrf,-8) -- fire
anim(e1,an.walklrh) -- head
anim(e1,an.walklrb,8) -- body
anim(e1,an.walklrt,16) -- tail

Here's a demo:

Cart [#19274#] | Copy | Code | 2016-03-17 | Link
7

In this demo, the player can walk around, while the enemy just cycles through his walking animations every 5 seconds. Press Z to toggle the player being in a "dead" state.

In the next part, we're going to talk about more advanced movement and collisions (and how they relate to each other), and get back into AI. They are already in the demo (except for the AI) if you'd like to get a head start, but are a topic for another blog entry. Stay tuned for those!

P.S. - You are free to use any of the code provided, but please don't use the graphics, as they are being used in my upcoming game. Thanks!

P#19275 2016-03-17 19:59

22

Allow me to preface this by saying that I'm new to Pico-8 and Lua, but have already fallen in love with it. I've already begun following the community on the BBS, subreddit, and Twitter, and I see a lot of questions about how to perform specific tasks that are necessary for almost every game. Anyone who has ever used a game engine previously may have had a lot of the basic things such as controls, animation, collision detection, or even AI handled for them, so they may not know how to roll their own code for such things.

I'm currently working on my first Pico-8 game, and have decided to document the process, and how I personally overcome various problems. Do know that my way is definitely not the only way - there may be solutions that work better, perform better, have smaller token counts, or are more elegant in general. These are just the solutions that I came up with. There isn't really any 'right' or 'wrong' per se, as long as it gets the job done, with the exception of 'lower token count = better' due to the limitations of the system. So, I'm going to try to solve these problems in the smallest possible token count that I can come up with.

Movement:

In almost every game, characters need to be able to move around the map. The most common control systems are 2-way (only up and down or left and right), 4-way (up, down, left, right), and 8-way (4-way + diagonals). Any of these are simple enough to do in Pico-8.

Here is a 2-way control system

function _init()
  player={} -- initialize the player object
  player.speed=1 -- set a property 'speed' to 1 in the player object
  player.x=0 -- set the initial x coordinate
  player.y=0 -- set the initial y coordinate
end

function _update()
  if(btn(0)) player.x-=player.speed -- move left at player.speed
  if(btn(1)) player.x+=player.speed -- move right at player.speed
end

Simple enough. Now 4-way:

function _init()
  player={} -- initialize the player object
  player.speed=1 -- set a property 'speed' to 1 in the player object
  player.x=0 -- set the initial x coordinate
  player.y=0 -- set the initial y coordinate
end

function _update()
  if(btn(0)) then player.x-=player.speed -- move left at player.speed
  elseif(btn(1)) then player.x+=player.speed -- move right at player.speed
  elseif(btn(2)) then player.y-=player.speed -- move up at player.speed
  elseif(btn(3)) then player.y+=player.speed -- move down at player.speed
  end
end

Using the elseif statements ensures that no more than one of these conditions can be met at a time, so it disallows diagonal movement. To allow diagonals for 8-way controls, simply change it to:

function _update()
  if(btn(0)) player.x-=player.speed -- move left at player.speed
  if(btn(1)) player.x+=player.speed -- move right at player.speed
  if(btn(2)) player.y-=player.speed -- move up at player.speed
  if(btn(3)) player.y+=player.speed -- move down at player.speed
end

Now multiple conditions are allowed to be met, allowing movement diagonally up+left, up+right, etc.

Animation:

Pico-8 doesn't currently supply any functions for animation, so you have to roll your own code for handling them. So, I wrote an animation function that anyone is free to use for their games, and it should be pretty simple to use and handle most use cases (currently it doesn't support sprites larger than 8x8 or any kind of sprite scaling, but if you need those things then it should be easy enough to modify).

-- object, starting frame, number of frames, animation speed, flip
function anim(o,sf,nf,sp,fl)
  if(not o.a_ct) o.a_ct=0
  if(not o.a_st) o.a_st=0

  o.a_ct+=1

  if(o.a_ct%(30/sp)==0) then
    o.a_st+=1
    if(o.a_st==nf) o.a_st=0
  end

  o.a_fr=sf+o.a_st
  spr(o.a_fr,o.x,o.y,1,1,fl)
end

To use it, simply do: anim(player,0,3,10), where the player is what we're animating, 0 is the animation starting frame (basically the offset), 3 is the number of frames in the animation, and 10 is the speed. The 'fl' (or flip) parameter is optional and defaults to false. If set to true, it'll flip the sprite horizontally. You can find more information about it here, and here it is in action:

Cart [#19159#] | Copy | Code | 2016-03-10 | Link
13

Collision:

To handle collisions with map tiles, you probably want to set a flag on the tile's sprite itself (those are the little colored buttons in the sprite editor under the scale slider - they start at 0 and go up to 7, so there are 8 possible flags in total). In this example, we're going to check for flag 0 using fget() and we're going to use mget() to see if the player's bounding box overlaps with a solid tile's bounding box.

We're additionally going to check for collisions with the world bounds as well, so that the player can't move outside of the world. Also, I wanted a way to be able to toggle collisions on or off with the player, either with map tiles, world bounds or both. So let's look at the setup:

function _init()
  w=128 -- width of the game map
  h=128 -- height of the game map
  player={}
  player.x=0
  player.y=0
  -- collide with map tiles?
  player.cm=true
  -- collide with world bounds?
  player.cw=true
end

Here's we're defining the width and height of the map so that we can check for wold bounds collisions, and then we're setting up 'player.cm' and 'player.cw' to define whether they should collide with map tiles and world bounds. Now let's look at the collision detection code itself:

function cmap(o)
  local ct=false
  local cb=false

  -- if colliding with map tiles
  if(o.cm) then
    local x1=o.x/8
    local y1=o.y/8
    local x2=(o.x+7)/8
    local y2=(o.y+7)/8
    local a=fget(mget(x1,y1),0)
    local b=fget(mget(x1,y2),0)
    local c=fget(mget(x2,y2),0)
    local d=fget(mget(x2,y1),0)
    ct=a or b or c or d
   end
   -- if colliding world bounds
   if(o.cw) then
     cb=(o.x<0 or o.x+8>w or
         o.y<0 or o.y+8>h)
   end

  return ct or cb
end

Basically all this does is check if we want to collide with map tiles, and then looks for an overlap. It does the same thing with world bounds as well. If there's a collision, it returns true, and if not, then it's false. To use it, we'll add it to our movement code:

function move(o)
  local lx=o.x -- last x
  local ly=o.y -- last y

  -- 8-way movement
  if(btn(0)) o.x-=o.speed
  if(btn(1)) o.x+=o.speed
  if(btn(2)) o.y-=o.speed
  if(btn(3)) o.y+=o.speed

  -- collision, move back to last x and y
  if(cmap(o)) o.x=lx o.y=ly
end

What we're doing is allowing the player to move into the tile, but since they will get moved back if there's a collision before the frame is drawn, you won't see them appear to 'bounce back out'. Here it is in action:

Cart [#19162#] | Copy | Code | 2016-03-10 | Link
10

You can read more about the collision function here.

AI:

AI is a more complex problem, and the solution really depends on your needs. For this case, we're going to be talking about Pacman AI, or how the pathfinding works and how to move the enemy around to chase the player. This is going to be rather complicated for me to try and articulate, even as simple as it is, but I'm going to do my best, so bear with me.

First, we need to start by thinking about what the rules are that the AI needs to follow. Here are the rules for my example AI:

  • Enemy cannot reverse directions (so it can't be moving left and then immediately turn around and start moving right).
  • Our target is the player, so it needs to find the shortest path to the player.
  • Enemy must keep moving, so if it reaches a target it shouldn't stop, but instead keep moving, looping around the shortest path around the target.

We could get into complex pathfinding algorithms such as A* or jump points, but I want to keep it oldschool. It's not going to be perfect like the more complex algorithms, but will find the correct path to the player the vast majority of the time. Look up "Pacman hiding spots", and you'll see that the algorithm we're about to use isn't perfect 100% of the time.

So here's how we're going to do it: whenever the AI has multiple directions it can move to (such as reaching an intersection), it will evaluate which tile that it has the option to move to is the shortest distance from the player. To do this, we use the Pythagorean Theorem, which is the square root of (from x - to x) squared + (from y - to y) squared. In Lua, that's sqrt((fx-tx)^2 + (fy-ty)^2), which returns the distance from a starting point to an ending point in a line. We can make a function out of this since we'll be using it to evaluate all of our optional directions:

function dst(fx,tx,fy,ty)
  return sqrt((fx-tx)^2+(fy-ty)^2)
end

But how do we know if we're at an intersection? We can use our collision code to check if the tiles around us are open!

local cl=colm(ex-1,ex-1,ey,ey+7)
local cr=colm(ex+8,ex+8,ey,ey+7)
local ct=colm(ex,ex+7,ey-1,ey-1)
local cb=colm(ex,ex+7,ey+8,ey+8)

It's messy, I know, but it works. "cl" means 'collision left', "cr" means 'collision right', etc., so we're adding 1 to our current position in each direction in order to see if we'd collide with that tile, and if not, it's open. Now that we know which tiles are open, we check their distances to the target using our dst() function:

local ld=dst(ex-4,tx+4,ey+4,ty+4)
local rd=dst(ex+11,tx+4,ey+4,ty+4)
local td=dst(ex+4,tx+4,ey-4,ty+4)
local bd=dst(ex+4,tx+4,ey+11,ty+4)

More messy code. The reason for the +/- 4's is that instead of using the top-left of our potential tiles that we can move to, I'm using the center point of them, so it's just an x/y offset. I'm not sure if it helps make it more accurate at all, but in my head it seems like it would help slightly.

The last thing we need to do to evaluate which direction to move in is to make sure that we aren't about to reverse directions. For that, I've set a property of my enemy object that tells me which direction it's moving in. So... the opposite of that direction is invalid, then.

local lo=not cl and e.m!=1 -- "left open" is true if there's no collision left and we're not moving right
local ro=not cr and e.m!=0 -- "right open" is true if there's no collision right and we're not moving left
local to=not ct and e.m!=3
local bo=not cb and e.m!=2

Now we need to know which of our valid directions is the shortest, using what we found for "ld", "rd", etc.:

-- shortest distance, I set it to map width here just to make
--it a number larger than than the possible distance between enemy and player
local sd=w

if(lo)           sd=ld
if(ro and rd<sd) sd=rd
if(to and td<sd) sd=td
if(bo and bd<sd) sd=bd

Now we need to set his moving direction for the next time this code runs, so he won't reverse directions on us. We'll also go ahead and move him:

if(lo and ld==sd) e.m=0
if(ro and rd==sd) e.m=1
if(to and td==sd) e.m=2
if(bo and bd==sd) e.m=3

if(e.m==0) e.x-=e.speed
if(e.m==1) e.x+=e.speed
if(e.m==2) e.y-=e.speed
if(e.m==3) e.y+=e.speed

Here it is in action:

Cart [#19156#] | Copy | Code | 2016-03-09 | Link
8

Whew! I hope that all made sense, but if not, let me know! I'll be happy to answer any questions, and amend the post as necessary to correct or clarify things.

I plan to keep documenting my progress and solutions to other problems as I find them throughout the development process of my game. Since this post is so huge, I will probably make continuations of it in new posts instead of adding further onto this monstrosity.

Here's some stuff I'll be trying to delve into for my game:

  • Procedural map generation
  • Multiple AI-controlled enemies, each having different targets (one might target the player's exact center, one might target the tile behind the player, one might target in front of the player, etc. to vary it up a bit and keep them from grouping together too much).
  • Changing between game states, such as title screen, menu, winning and losing, etc.
  • Implementing a demo/attract mode in which the player is AI-controlled
  • Managing a player inventory and item pickups
  • Multiple AI modes (chase, scared/fleeing)
  • If the AI has multiple path options that are equidistant, choose a random one to make it less predictable
  • Increasing the difficulty of the game as players progress through levels
  • Scoring and keeping a hiscore
  • Adding sound effects and music (although I likely won't get into how to use these tools, as there are already great tutorials out there for that)
  • More as I begin to flesh out my game a little better

Thanks for reading, and I hope this helps someone! Happy coding :)

P#19166 2016-03-10 14:38

10

Cart [#19162#] | Copy | Code | 2016-03-10 | Link
10

I've seen some questions about how to do collision detection in Pico-8, so figured I'd make another bare-bones demo, this one demonstrating collision detection with map tiles and/or world bounds. The function itself is 24 lines and 125 tokens, and includes flags for turning collisions on or off on an object (such as the player). Here's the full function:

function cmap(o)
  local ct=false
  local cb=false

  -- if colliding with map tiles
  if(o.cm) then
    local x1=o.x/8
    local y1=o.y/8
    local x2=(o.x+7)/8
    local y2=(o.y+7)/8
    local a=fget(mget(x1,y1),0)
    local b=fget(mget(x1,y2),0)
    local c=fget(mget(x2,y2),0)
    local d=fget(mget(x2,y1),0)
    ct=a or b or c or d
   end
   -- if colliding world bounds
   if(o.cw) then
     cb=(o.x<0 or o.x+8>w or
           o.y<0 or o.y+8>h)
   end

  return ct or cb
end

It will return true if a collision is detected, and the object has the flag(s) set for collisions. To set up the function for use, you need the following global variables/properties set:

w -- the width of your map
h -- the height of your map
[object].cm -- whether the object should collide with map tiles
[object].cw -- whether the object should collide with the world bounds (as defined above in w and h)

To call the function, simply use: cmap([object]).

P#19163 2016-03-10 12:17

13

Cart [#19159#] | Copy | Code | 2016-03-10 | Link
13

This is a very simple animation function that anyone can use in their games. It's a bare-bones demo to keep everything as simple as possible. The function itself is only 14 lines and 74 tokens, and works like this:

anim(object, start frame, number of frames, speed (in frames per second), [flip])

In the demo, the object is the player, but it can be anything. Start frame is where the animation to be played begins (so in this case, moving left/right starts on frame 6, so we have anim(player,6,[..])). Number of frames is how many frames in that animation to play. We have 3, so anim(player,6,3,[..]). Speed is how fast the animation should play in frames per second (I have it set to 10 here). Lastly, flip is whether or not to flip the sprite horizontally (this parameter is optional, and the default is false).

To use it in your project, you only need to copy the following code, and call it as described above:

function anim(o,sf,nf,sp,fl)
  if(not o.a_ct) o.a_ct=0
  if(not o.a_st) o.a_st=0

  o.a_ct+=1

  if(o.a_ct%(30/sp)==0) then
    o.a_st+=1
    if(o.a_st==nf) o.a_st=0
  end

  o.a_fr=sf+o.a_st
  spr(o.a_fr,o.x,o.y,1,1,fl)
end

I hope this helps some of you who have had questions about how to animate in Pico-8. Good luck and have fun! :)

P#19160 2016-03-10 09:50

8

Cart [#19156#] | Copy | Code | 2016-03-09 | Link
8

What is it?

It's not a game, but rather a framework for a specific genre of games ala Pac-Man and Burgertime. It's a complete skeleton framework with all of the basic functionality to create these types of games.

Features:

  • A sample map with collision detection
  • Outside of map bounds = collision; nothing can move out of bounds
  • A player control system with automated assisted controls (it's difficult to line up exactly with a new corridor, so the controls will auto-correct to anticipate and compensate for this when a collision is detected)
  • Enemy AI and movement in "oldschool" style; it does not use A* or other complex pathfinding algorithms, but instead evaluates the closest next direction to move in when it detects an intersection, which is what the oldschool games did.
  • Enemy AI cannot reverse directions (classic Pac-Man Ai rule)
  • You can change the speed of the AI or player (as low as 0.1, and up to a speed of 2) without negatively affecting the collisions or AI.
  • Can easily support multiple AI-controlled units. Just call another eupd() for each of your AI characters.
  • It's currently 691 tokens

Planned features for future revisions:

  • Random map generation would be pretty cool as long as it doesn't make the engine much more token heavy than it already is.
  • I'd like to get it down to 500 tokens or less, if possible, which would definitely allow me to add more features comfortably.
  • Support for chase, scatter and scared (flee) modes?

I don't want to add too much more than that to it. It's intended to be small and then built upon on a case-by-case basis.

Known issues (updated Mar 8, 2016):

  • I don't think the AI will handle hitting a dead-end, so in its current form, I'm pretty sure any dead ends in your maps will break the AI (actually it might just start ignoring collisions and plow its way through the dead end). I just need to code in an exception to the rule that it can never reverse directions (and actually in Pac-Man for example, the AI could reverse direction during certain cases anyway, such as when you eat a power pellet, or on a timer where the AI enters 'scatter' mode).
  • A speed of 2 seems to be about the fastest that the collision system will support before it starts getting a bit wonky. 2 feels like a very fast speed though, so maybe I can let this bug slide for this use case. Let me know your thoughts.
  • A speed of less than 0.5 on the player seems to cause the assisted controls system to become unreliable. I'm not sure why anyone would want to make the player move around that slowly (seems it would be quite boring), but the issue is there nonetheless.
  • I'd like to improve the comments in the code. There isn't much there in the way of comments, and my naming conventions for variables and functions was mostly abbreviations or shortened versions of words, so it's not the most readable code in the world.

Can it easily support 8-direction movement, or is it just for mazes/corridors?

Yes, it can easily support 8-direction movement. Go to line 50, and change:

if(btn(0)) then p.x-=p.s
elseif(btn(1)) then p.x+=p.s
elseif(btn(2)) then p.y-=p.s
elseif(btn(3)) then p.y+=p.s
end

to:

if(btn(0)) p.x-=p.s
if(btn(1)) p.x+=p.s
if(btn(2)) p.y-=p.s
if(btn(3)) p.y+=p.s

This will allow 8-direction movement for the player, and the AI supports it out of the box already. Be aware that enabling 8-direction movement in a corridor-based maze map such as the example provided, will sometimes cause players to move diagonally at intersections if they are holding down multiple directional buttons. The behavior feels weird in these types of games, so I have it set for 4-directions by default, but 8-direction would work well for roguelikes or Zelda-inspired adventure games.

What do I put for the CC-BY attribution if I use this in my game?

Just credit @clowerweb (my Twitter handle) as a programmer or something similar.

If you like the engine, or find any bugs or can help to reduce the token count or optimize performance, I'd love any and all input. I just started this project today and have no prior experience with Lua, so please forgive any herpyderpies. I hope to see some people make some great games with this!

Update March 9th: Reduced the token count from ~730 to ~690. No other major changes.

engine pac-man burgertime maze ai
P#19115 2016-03-07 23:38


:: More
X
About | Contact | Updates | Terms of Use
Follow Lexaloffle:        
Generated 2017-05-22 21:30 | 0.308s | 2097k | Q:59