Welcome, Guest. Please login or register.
Did you miss your activation email?

Login with username, password and session length

 
Advanced search

877350 Posts in 32857 Topics- by 24297 Members - Latest Member: laffertyr

May 19, 2013, 09:53:42 AM
TIGSource ForumsDeveloperTechnical (Moderators: Glaiel-Gamer, ThemsAllTook)AS3 Challenge! - Snake!
Pages: 1 [2] 3
Print
Author Topic: AS3 Challenge! - Snake!  (Read 2901 times)
Sam
Level 3
***



View Profile WWW
« Reply #15 on: February 11, 2012, 05:42:34 AM »

Does the following line fix it if I add it to my key listener:
Code:
event.stopPropagation();

The .stopPropagation method will prevent that particular event from bubbling any further up the display list.

This reply is about to get off the topic of your particular problem, but hopefully will be useful general Flash event handling stuff.

Say you have a display list like this (anne and conique are direct children of stage, ben is a child of anne)

stage
  |
  |___ anne
  |      |
  |      |___ ben
  |
  |___ conique

 
If a KeyboardEvent is dispatched from Ben it will first trigger any listeners on Ben, then "bubble" up to Anne triggering any listeners on her, then bubble up to stage and trigger any listeners there. Conique will have no idea that the KeyboardEvent ever happened.
This is why if you want to catch all the keyboard events that happen you usually add your listeners to the stage or some other similarly base-level DisplayObject.

If you have a listener on Anne which calls the .stopPropagation method on the event then it will be stopped there, and will not bubble up to the stage.

Some caveats:
Not all events do the bubbling phase; you can see if one does by examining the read-only property bubbles.
Not all events can have their propagation stopped; again you can check by examining the read-only property cancelable.

The Source of KeyboardEvents
Something interesting to know about is which object Flash dispatches KeyboardEvents from. They are dispatched from whatever InteractiveObject is indicated by your stage's focus property, or the stage itself if that property is null. You can detect when focus changes by listening for FocusEvent.FOCUS_IN for an object gaining focus and/or FocusEvent.FOCUS_OUT for an object losing focus. These events bubble, so if you place your listeners on the stage you should hear about all of them (or at least about all the FOCUS_IN events).

You'll find that focus will generally change to whatever InteractiveObject was last clicked on or selected using the tab key (to be more accurate, it changes when you click on a TextField, but will revert to null on clicking a Sprite - I'm unsure how exactly it is determined what objects will automatically shift focus). The reason for this is so that textfields and textfield-like things can be sure that they receive keyboard input after they've been selected by the user.

Most of the time this doesn't matter in games, as we're just listening on the stage, and bubbling events always eventually reach the stage, right? Almost always. Say Ben is the focus, and he is removed from the display list. KeyboardEvents are still going to be dispatched from him, but as he's now disconnected from the display list they will have nowhere to bubble to and will go unheard by our listeners on the stage. Until the user next clicks somewhere, you'll miss out on all their keyboard input.

There's two ways to prevent this:
Make a little utility class that listens on the stage for FOCUS_IN events and keeps track of that object, manually resetting focus to null if that object dispatches an Event.REMOVED_FROM_STAGE. Just make sure you don't end up leaving event listeners attached to objects all over the place.
Simply set stage.focus = null every frame. Simple, but that will totally break any input TextFields you might be using.

And now back to the actual question at hand.

Basically, no.

Simplest solution would be to have a Boolean variable hasTurnedThisUpdate, set to false at the start of each frame and have your event listeners check that it's false before turning the snake (and set it to true once they do). Problem is if a user presses two different directions in quick succession this will effectively ignore the second input, which leads to controls feeling buggy and unresponsive.

So instead! You'd want to create a queue structure to hold incoming input commands. Using an Array or Vector should be fine (if you're chasing performance queues are best implemented as linked lists but it'll really not matter in this case), and have your KeyboardEvent listener just .push the event it receives on to the back of the queue. Your main game loop will then .shift the front event from the queue and deal with it. This way there's no lost keyboard events, and you always have a maximum of one event to deal with each update.

Problems with this solution that are "left as an exercise for the reader":
If the user mashes a load of unrelated buttons and then presses the key to turn Left, your game loop will take several frames before it gets around to dealing with the Left command. Laggy controls!
Similarly (and slightly less contrived) if the player is holding down the Down key and then presses Left then depending on their system your queue may be filled with dozens of Down key events which will delay reception of the Left key event. Again leading to the feeling of laggy controls.

(You'll want to either filter what key events are added to the queue by your listener, or allow the game loop to intelligently deal with multiple key events each loop - but without reintroducing the original instant U-turn problem.)
Logged
Moczan
Level 5
*****



View Profile
« Reply #16 on: February 11, 2012, 06:30:18 AM »

You really should just have bools for every direction (like LEFT and RIGHT in my code), check them in your main loop in if...else conditional tree, excluding direction opposite to the current one. It won't accidentally kill you and will let you do fast u-turns.
Queues are useful if you need to store input chains for combos in fighter games etc. no need to implement such complex thing in such simple game as Snake.
Logged
Sam
Level 3
***



View Profile WWW
« Reply #17 on: February 11, 2012, 06:55:02 AM »

This issue is if in a traditional grid-based Snake the player's currently moving up, then presses Right then Down so fast that they occur within a single frame length.

For instance if you're doing something like this for handling input:
Code:
static const DIR_RIGHT:int = 0;
static const DIR_UP:int =    1;
static const DIR_LEFT:int =  2;
static const DIR_DOWN:int =  3;

var keyCodeForRight:int = 39;
var keyCodeForUp:int    = 38;
var keyCodeForLeft:int  = 37;
var keyCodeForDown:int  = 40;

var currentDirection:int = DIR_UP;

function keyEventListener(e:KeyboardEvent):void
{
   switch (e.keyCode)
   {
      case keyCodeForRight:
         if (currentDirection != DIR_LEFT)  currentDirection = DIR_RIGHT;
         break;
      case keyCodeForUp:
         if (currentDirection != DIR_DOWN)  currentDirection = DIR_UP;
         break;
      case keyCodeForLeft:
         if (currentDirection != DIR_RIGHT) currentDirection = DIR_LEFT;
         break;
      case keyCodeForDown:
         if (currentDirection != DIR_UP)    currentDirection = DIR_DOWN;
   }
}

function perFrameUpdate():void
{
   switch (currentDirection)
   {
      case DIR_RIGHT:
         playerX++;
         break;
      case DIR_UP:
         playerY--;
         break;
      case DIR_LEFT:
         playerX--;
         break;
      case DIR_DOWN:
         playerY++;
   }
}

Then it is possible with sufficiently fast key presses and sufficiently low framerate for keyEventListener to receive an event for the right key and an event for the down key before perFrameUpdate is called. As the events are received by keyEventListener in the correct order its "no doubling back" logic will still allow it.

However as the player hasn't actually moved between those two inputs the result is that the player performs a suicidal instant U-turn.

You can avoid it by adding a limit of one turn per frame.  But as I've said above that leads to the game ignoring user input, hence the need for a queue of some description.

Edit:
I should add that if you shift the "no doubling back" logic to the perFrameUpdate function, like so:

Code:
static const DIR_RIGHT:int = 0;
static const DIR_UP:int =    1;
static const DIR_LEFT:int =  2;
static const DIR_DOWN:int =  3;

var keyCodeForRight:int = 39;
var keyCodeForUp:int    = 38;
var keyCodeForLeft:int  = 37;
var keyCodeForDown:int  = 40;

var directionFromInput:int = DIR_UP;
var currentDirection:int = DIR_UP;

function keyEventListener(e:KeyboardEvent):void
{
   switch (e.keyCode)
   {
      case keyCodeForRight:
         directionFromInput = DIR_RIGHT;
         break;
      case keyCodeForUp:
         directionFromInput = DIR_UP;
         break;
      case keyCodeForLeft:
         directionFromInput = DIR_LEFT;
         break;
      case keyCodeForDown:
         directionFromInput = DIR_DOWN;
   }
}

function perFrameUpdate():void
{
   // handle input
   switch (directionFromInput)
   {
      case DIR_RIGHT:
         if (currentDirection != DIR_LEFT)  currentDirection = DIR_RIGHT;
         break;
      case DIR_UP:
         if (currentDirection != DIR_DOWN)  currentDirection = DIR_UP;
         break;
      case DIR_LEFT:
         if (currentDirection != DIR_RIGHT) currentDirection = DIR_LEFT;
         break;
      case DIR_DOWN:
         if (currentDirection != DIR_UP)    currentDirection = DIR_DOWN;
   }

   // actually move the player
   switch (currentDirection)
   {
      case DIR_RIGHT:
         playerX++;
         break;
      case DIR_UP:
         playerY--;
         break;
      case DIR_LEFT:
         playerX--;
         break;
      case DIR_DOWN:
         playerY++;
   }
}

Then you avoid the possibility of suicidal instant U-turns, but the game will again ignore some input. In the danger case of the player travelling up and rapidly pressing Right then Down, when perFrameUpdate is called directionFromInput will be DIR_DOWN as that was the last keyboard input received. The result will be that the "no doubling back" logic detects a deadly U-turn, and will refuse to make the turn.

However that means that the game has now ignored two valid user key-presses, with the snake continuing on up after the user has instructed it to turn right and down. Even more infuriating for our user, if she'd made the exact same input but a few milliseconds earlier or later then the two inputs would have fallen in different update cycles and the game would have responded correctly. A classic "sometimes the controls just don't work at all" bug.
« Last Edit: February 11, 2012, 07:19:12 AM by Sam » Logged
Moczan
Level 5
*****



View Profile
« Reply #18 on: February 11, 2012, 09:11:44 AM »

Sam you still ignored my solution using Boolean variables. If you check your logic against input directly in KeyboardEvent handler, you will always approach problems as it's not the way you should do it.

Implementing a simple getClick (or rather queueClick would be better name) method solves this problem.

Pseudo code of it would be something like
Code:
function keyDown(e.KeyboardEvent):void{
  keyClick[e.keyCode] = true;
}
function getClick(arg:uint):Boolean{
  if(keyClick[arg]){
    keyClick[arg] = false;
    return true;
  }
  return false;
}
Now if you check the movement in your game loop it will handle the fast turns situations without any problems and suicide moves.

Now if you check the movement in your game loop it will handle the fast turns situations without any problems and suicide moves.

If there is anything wrong, it's the input check in gameloop, not the KeyboardEvent handler. You are solving problems in a wrong place Wink
« Last Edit: February 11, 2012, 09:29:45 AM by Moczan » Logged
randomshade
Level 1
*


Fastzelda


View Profile WWW Email
« Reply #19 on: February 11, 2012, 09:57:36 AM »


This is probably my favorite one so far. Feels traditional in terms of control/movement while being off the grid.

@ashkin: I would suggest having a look at some other peoples code to take a look at how raw as3 would work, but if you want to have a go using flixel or flashpunk, noones stopping you; this is about fun, not beauriocracy Smiley

Yeah, I'm more interested in seeing interesting ideas than anything else here. If you take a look at my code (which isn't amazing, but works) I set up everything as a sort of ultra-lite game engine. So a lot less functionality than Flixel or whatever, but takes care of some of the menial stuff.

Edit: I should add that I'm not an expert in AS3 and it's not my primary dev language. I've just been tinkering with it for a while now.
« Last Edit: February 11, 2012, 10:03:51 AM by randomshade » Logged

Sam
Level 3
***



View Profile WWW
« Reply #20 on: February 11, 2012, 12:32:02 PM »

I like your "has this button been pressed since I last asked?" function, and use something very similar in a general key handling class I use (although that kind of "destructive" function does have to be used with care). However I don't see how that's going to avoid cases where the game will ignore user input.

Taking the same danger case:
Snake is moving upwards, user presses Right then Down really fast such that those events are both received before the next main game loop is called.

Assuming your main loop would have something like:

Code:
if (getClick(keyCodeForUp) && (currentDirection != DIR_DOWN))
{
   currentDirection = DIR_UP;
}
else if (getClick(keyCodeForDown) && (currentDirection != DIR_UP))
{
   currentDirection = DIR_DOWN;
}
else if (getClick(keyCodeForRight) && (currentDirection != DIR_RIGHT))
{
   currentDirection = DIR_RIGHT;
}
else if (getClick(keyCodeForLeft) && (currentDirection != DIR_LEFT))
{
   currentDirection = DIR_LEFT;
}

// and a switch or something to actually move the player depending on currentDirection

As the inputs came really fast, getClick(keyCodeForRight) and getClick(keyCodeForDown) would both return true on this iteration of the main loop.

In this particular instance (and this particular ordering of if/else statements) down is checked first, and denied as the snake's going the opposite direction. Right will then be checked and allowed. So the end result is the snake turns and moves right.

As calling getClick(keyCodeForDown) causes future calls to it to return false until another key press is detected, that first down key press has been lost. So again we're back to the issue of a valid player input being ignored.

If our if/else statements in the above code happened to be in another order then we'd actually get the correct response from the game. If it checked Right first, then Down it would correctly turn right in one update iteration then down in the next (as it wouldn't call getClick(keyCodeForDown) on the first iteration, and so wouldn't lose the record of that input). Of course we'd still have the same problem occurring for some other combination of directions and inputs, but it gave me an idea:

If the getClick function simply returned the value for that key without automatically changing it to false (i.e. make the function call non-destructive) then we can get the behaviour we want without needing to use a proper queue. For simplicity of writing, this code just directly accesses the keyClick array.

Code:
if (keyClick[keyCodeForUp] && (currentDirection != DIR_DOWN))
{
   currentDirection = DIR_UP;
   keyClick[keyCodeForUp] = false;
}
else if (keyClick[keyCodeForDown] && (currentDirection != DIR_UP))
{
   currentDirection = DIR_DOWN;
   keyClick[keyCodeForDown] = false;
}
else if (keyClick[keyCodeForRight] && (currentDirection != DIR_RIGHT))
{
   currentDirection = DIR_RIGHT;
   keyClick[keyCodeForRight] = false;
}
else if (keyClick[keyCodeForLeft] && (currentDirection != DIR_LEFT))
{
   currentDirection = DIR_LEFT;
   keyClick[keyCodeForLeft] = false;
}

// and a switch or something to actually move the player depending on currentDirection

The difference from the previous code is that the record of an input is only removed when that input is acted upon, meaning we don't ever ignore user input. That'll work for our test case, but now that I've typed it out I realise it gives a fresh new error case:

If the player is moving upwards, presses down then presses left with any length of a delay between the two events then they will turn downwards one frame after they turn left, even if the Down event was several seconds earlier. Now we're misinterpreting user input.


What a lot of text, read this bit for the important point:

Considering the problem on a slightly higher level, the order of key presses matters.

Our test case is a snake moving up, then the player pressing Right followed by Down with the key events occurring before the next iteration of the main update loop. This should make the snake do a non-suicidal U-turn. However if the player had pressed Down followed by Right, the snake should just turn right. No solution that merely looks at what keys have been pressed since the last update could differentiate between those two cases.

We need a solution that takes the order of key presses into account. A queue seems the clearest solution for that, although others certainly exist.
« Last Edit: February 11, 2012, 12:37:31 PM by Sam » Logged
buranmu
Level 0
*


View Profile
« Reply #21 on: February 11, 2012, 04:27:54 PM »


Does the following line fix it if I add it to my key listener:
Code:
event.stopPropagation();



Sam gave a awesome explanation of this already, and as he said, the event propagation doesn't prevent that from happening. It's actually completely unneeded in my code, and it's just a (perhaps bad?) habit I've acquired from working with as3.

To solve that problem, I just don't update the direction until each enterFrame event. I store the most recent direction that the player inputs (ignoring illegal/suicide inputs). So even if you were going left and then hit up and right before the next event, it would still be comparing against the left direction until that next event actually happens. I haven't fully read through the boolean/queue discussion about what is the best method, but to me this very straightforward and easy to understand, and it felt appropriate at the time -- I'm sure there are more robust options out there. Hand Thumbs Up Right
Logged
Moczan
Level 5
*****



View Profile
« Reply #22 on: February 12, 2012, 03:08:48 AM »

Yeah, I'm more interested in seeing interesting ideas than anything else here. If you take a look at my code (which isn't amazing, but works) I set up everything as a sort of ultra-lite game engine. So a lot less functionality than Flixel or whatever, but takes care of some of the menial stuff.

Edit: I should add that I'm not an expert in AS3 and it's not my primary dev language. I've just been tinkering with it for a while now.

Yep, that's why I tried the non-grid Snake and did rendering using BitmapDatas only, to show an interesting variation on a classic and to teach those with less AS3 experience the basics of Bitmap rendering. I didn't comment it at all, but tried to pack everything into self-explanatory function, if somebody has any question about the code, feel free to ask  Wink


Logged
msilver
Level 1
*



View Profile WWW Email
« Reply #23 on: February 12, 2012, 03:43:07 AM »

@Sam Thanks for the explanation of how events are handled in flash. It makes sense when you think about it now that I've heard the explanation. You talk of objects that have left the stage not propagating their key events, but wouldn't they also not be drawn or selectable as well? Unless you are telling me that a text field could have focus, be removed from the stage and thus become invisible and unselectable but will still exist and keep focus?

Of course I thought stop propagation might just stop keyboard events till the next frame... that wouldn't have been the best solution either as you've discussed.

@randomshade and @Moczan Non grid is cool.

My proposed solution to the input problem:

Since everyone likely has an as3 "Array" containing the snake segments, in the key handler we simply check to see if moving once in the proposed new input direction will hit the second element in the snake list. It removes the need for queues and added variables and so on. It also does not ignore input if the user has some kind of magic fast fingers and presses a valid direction and then the suicide direction.
Logged
Sam
Level 3
***



View Profile WWW
« Reply #24 on: February 12, 2012, 04:18:00 AM »

Unless you are telling me that a text field could have focus, be removed from the stage and thus become invisible and unselectable but will still exist and keep focus?

Yup, exactly. You can make a test to demonstrate it: Create and add a TextField, create KeyboardEvent listeners on the stage, and use a timer to remove the TextField after a couple of seconds of execution.

If the user clicks the TextField before it's removed from stage (and doesn't do any further clicking afterwards) then you'll find that your KeyboardEvent listeners suddenly stop receiving any events. You can also check and see that the stage's focus property will still be referencing the TextField. As soon as the user clicks anywhere within the SWF focus will be set to either some child of the stage or to null, and the event listeners will hear the keyboard events again.

It sounds like a fairly contrived example, but it can happen if you're using TextFields to show floating damage or XP numbers (although they might have to be set to Input mode in order to capture focus) and the user clicks on one before it fades out and is removed from the stage.

I mentioned you can make a simple class to monitor and "protect" the focus without messing up the proper focus-setting behaviour for TextFields and other interactive elements that need keyboard input. Here's a copy of mine:
Code:
public class FocusProtector
{
private var stage:Stage;
private var currentFocus:InteractiveObject;

public function FocusProtector(stage_:Stage)
{
stage = stage_;
stage.addEventListener(FocusEvent.FOCUS_IN, focusInListener);
}

private function focusInListener(e:FocusEvent):void
{
var newFocus:InteractiveObject = e.target as InteractiveObject;

if (currentFocus != null)
currentFocus.removeEventListener(Event.REMOVED_FROM_STAGE, removedFromStageListener);

newFocus.addEventListener(Event.REMOVED_FROM_STAGE, removedFromStageListener);
currentFocus = newFocus;
}

private function removedFromStageListener(e:Event):void
{
stage.focus = null;
}
}

My proposed solution to the input problem:

Since everyone likely has an as3 "Array" containing the snake segments, in the key handler we simply check to see if moving once in the proposed new input direction will hit the second element in the snake list. It removes the need for queues and added variables and so on. It also does not ignore input if the user has some kind of magic fast fingers and presses a valid direction and then the suicide direction.

 _ _ _  0 ms after last update
|_|_|_|
|_|O|_|  A snake travelling upwards
|_|O|_|

 _ _ _  3 ms after last update
|_|_|_|  INPUT ONE:
|_|O|A|  User presses right, we check that A is free
|_|O|_|    It is, so snake's direction is changed to Right

 _ _ _  8 ms after last update
|_|_|_|  INPUT TWO:
|_|O|_|  User presses down, we check that B is free
|_|B|_|     It's filled with snake, so the direction is invalid, snake continues Right

My issue is that I consider INPUT TWO to be a valid input, and that by interpreting it as the player attempting a suicidal U-turn (and so declaring it an invalid input) we are making an error.

I hold this position because if the user made precisely the same input sequence, but started it 10ms later (assuming we're updating state every 16ms) then it would be a valid input:

 _ _ _  0 ms after last update
|_|_|_|
|_|O|_|  A snake travelling upwards
|_|O|_|

 _ _ _  13 ms after last update
|_|_|_|  INPUT ONE:
|_|O|A|  User presses right, we check that A is free
|_|O|_|    It is, so snake's direction is changed to Right

--Update!--

 _ _ _  0 ms after last update
|_|_|_|
|_|O|O|  A snake travelling to the right
|_|O|_|

 _ _ _  2 ms after last update
|_|_|_|  INPUT TWO:
|_|O|O|  User presses down, we check that B is free
|_|O|B|     It is, so snake's direction is changed to Down

It seems ugly to me that the same input sequence would have different results depending on when during the update cycle it is made.
Logged
msilver
Level 1
*



View Profile WWW Email
« Reply #25 on: February 12, 2012, 05:00:41 AM »

Ok, that is weird about the keeping focus after stage removal. Ah AS3.

Back to input. I think you're thinking of fighting games more than snake. The point of snake to me is that you have to time your key presses perfectly based on what you see. If there has been no update then you pressed too soon. In a fighting game it would be an obviously huge mistake to loose a key press.

If we did the queue thing I would call it "fighting snake" where you can jam in key presses to almost program the snake's future movements like a fighting game.

Edit:(FIX HORRIBLE GRAMMAR MISTAKE)
Logged
Sam
Level 3
***



View Profile WWW
« Reply #26 on: February 12, 2012, 06:11:27 AM »

The point of snake to me is that you have to time your key presses perfectly based on what you see. If there has been no update then you pressed too soon.

Ah, that's fair enough then. Most programming decisions end up being game design decisions in the end.  Hand Thumbs Up Right
Logged
randomnine
Level 1
*


View Profile WWW
« Reply #27 on: February 13, 2012, 04:23:01 PM »

Here's mine. Spent the weekend on it, though I did play around with some advanced mechanics to make things more interesting and use this as an opportunity to play with Pixel Bender. Sorry - I got into it and ended up polishing it a fair bit!

Playable SWF and source code (source code in .zip at bottom of page)

It's in HaXe, not AS3, but they're very similar languages. It also uses some basic engine code I use for all my games.

This queueing discussion wasn't here when I started. Shocked I queue one move ahead because pulling off intricate manoeuvres makes people feel awesome, and I want my players to feel awesome. Precision moves in Snake are hard enough. I think it's always worth facilitating awesomeness where you can.

I switched to interpolating the snake's head and tail between movement frames because it felt better and made it easier to time key presses. It's pretty easy to add this just as an interpolation step in the rendering without changing the game logic.

On the design side: My play field's only 18 x 13 and there are five apples, to get people doing lots of tight manoeuvres even when it's easy early on. If the play field's too big and empty there's no urgency to anything and the game becomes a grind to get to the hard part.
Logged

jotapeh
Level 10
*****



View Profile WWW Email
« Reply #28 on: February 13, 2012, 04:49:46 PM »

randomnine that is a gruesome but awesome snake variant.
Logged

randomshade
Level 1
*


Fastzelda


View Profile WWW Email
« Reply #29 on: February 13, 2012, 05:27:56 PM »

@randomnine: Awesome  Evil
Logged

Pages: 1 [2] 3
Print
Jump to:  

Theme orange-lt created by panic