6
\$\begingroup\$

I'm coding "Snake" as my first game and I'm facing the logical fail when pressing multiple keys simultaneously eg. { up, right}; {up, left}; {down, right}, etc. Snake is able to go back and forth and eat his own body (that's not supposed to be).

My question is how can I write the keypress condition for only one press per one update is allowed?

Thank in advance.

public void update(GameContainer gc, StateBasedGame sbg, int delta) throws SlickException {
    if (dir == DOWN ) {
        head = new Point(head.getX(), head.getY() + SPEED * delta);
    }
    if (dir == UP) {
        head = new Point(head.getX(), head.getY() - SPEED * delta);
    }
    if (dir == RIGHT) {
        head = new Point(head.getX() + SPEED * delta, head.getY());
    }
    if (dir == LEFT) {
        head = new Point(head.getX() - SPEED * delta, head.getY());
    }       

}
public void keyPressed (int key, char c) {
    if ((key == Input.KEY_LEFT) && dir != RIGHT) {
        dir = LEFT; 
    }
    if ((key == Input.KEY_RIGHT) && dir != LEFT) {
        dir = RIGHT; 
    }
    if ((key == Input.KEY_UP) && dir != DOWN) {
        dir = UP; 
    } 
    if ((key == Input.KEY_DOWN) && dir != UP) {
        dir = DOWN; 
    }

}
\$\endgroup\$
  • 8
    \$\begingroup\$ Congratulations for choosing a game as simple as Snake as your first game, it's very simple, yet it has its challenges :) \$\endgroup\$ – Vaillancourt Sep 17 '15 at 11:37
  • 1
    \$\begingroup\$ This bug is still burdening my mind. But in case I make it out, do you have any suggestion which game should I try next? @AlexandreVaillancourt \$\endgroup\$ – NERDYLIZARD Sep 17 '15 at 11:50
  • 2
    \$\begingroup\$ I would suggest something like a Breaktout clone or an Asteroids clone.. Anything that would introduce basic physics replication :) Anyways, it's always up to you. \$\endgroup\$ – Vaillancourt Sep 17 '15 at 11:57
  • 1
    \$\begingroup\$ What do you mean "goes back and forth"? How did you implement collision? \$\endgroup\$ – MatthewRock Sep 17 '15 at 12:13
  • 1
    \$\begingroup\$ I have added another option in my answer. This new one only changes a little your current implementation. \$\endgroup\$ – Vaillancourt Sep 17 '15 at 14:31
5
\$\begingroup\$

Your issues does not seems to be "How do I accept only one directional input per update?" but rather "How do I prevent my snake to go backward?"

From the many comments and discussion, you seem to be having a frame-rate that is not appropriate for the game you're developing. Traditionally, Snake is a game with a very low frame-rate, the snake moved only 2-3 times per second.

With current computer capabilities, we can achieve a much higher frame-rate. This allows to have much smoother animations which are more pleasing to the eye.

However, the base logic for a snake game has not changed. The snake must be traveling on a grid and some care must be taken to make it move on that grid.

Here is the base logic that I wanted to illustrate with my comments on the answer by @Blue (which is a very fine answer, by the way).

This is pseudo code and logic:

FRAME_RATE       = 30   
TILE_SIZE        = 30   // Visual positions of where the snake can be
BOARD_SIZE_PX    = 600  // Size of the board in pixels, so a tile is 
                        // BOARD_SIZE_PX / GRID_SIZE --> 20 px
TILE_SIZE_PX     = BOARD_SIZE_PX / GRID_SIZE
SNAKE_SPEED_TILE = 2    // A snake can travel 2 tile every second
SNAKE_SPEED_RATE = FRAME_RATE / SNAKE_SPEED_TILE // 15 frames for traveling 1 tile

main
  // setup stuff

  frameCount = 0
  currentHeading = [random heading]
  nextHeading = currentHeading
  while( true )

    updateInput( currentHeading, nextHeading )

    snake_frame = frameCount % SNAKE_SPEED_RATE
    if ( snake_frame == 0 )
      // This is the moment to update the logic of where the snake is
      // and where it must go next
      updateSnakeLogicalPosition( nextHeading )
      currentHeading = nextHeading

    updateSnakeVisualPosition( snake_frame )

    frameCount++

updateInput ( in_currentHeading, inout_nextHeading )
  // is there an input, if no, the inout_nextHeading remains unchanged
  // if not, update the inout_nextHeading if the desired heading from keyboard 
  // if it is not going backwards (according to the in_currentHeading)

updateSnakeLogicalPosition( in_nextHeading )
  // Actualy move the locigal position of the snake according to the in_nextHeading
  // In theory, it is not possible at this point that your snake goes backwards
  // because you have made a check agains that in your input method. 

updateSnakeVisualPosition( in_snake_animation_frame )
  animation_percent = in_snake_animation_frame / SNAKE_SPEED_RATE
  offset_in_pixels = animation_percent * TILE_SIZE_PX

  // The offset_in_pixels is the distance into the tile. 
  // Now update the head and the tail tip according to that value.

Of course, this is assuming a 'traditional' snake, where the position of the snake is very discrete on a grid. And this is also assuming that you have added a smooth animation feature for your snake.


Addendum

Based on your latest comments I understand a bit more your situation. This here is more specific to your situation.

The idea is still to limit the frequency at whcih the snake is updated. To achieve this in your framework I suggest adding an 'time' accumulator that will track when it's time to update the position of the snake.

This is to emulate the comment from @Blue to emulate a low frame-rate since you don't seem able to limit it yet.

I haven't tested this code:

int mLastDir = [random initial dir];
int mMsAccumulator = 0;
const int SNAKE_RATE = 2; // Times the snake will move each second
const int SNAKE_MOVE_EACH_MS = 1000 / SNAKE_RATE; // The snake will move once 
                                                  // each of these ms
const int SNAKE_SPEED_FRAME_PX = 20; // This is realy up to you: how many px 
                                     // the snake moves each time it moves, 
                                     // based on the size of your tiles

public void update(GameContainer gc, StateBasedGame sbg, int delta) throws SlickException {
    mMsAccumulator += delta; // Accumulate to determine when we're ready
    if ( mMsAccumulator < SNAKE_MOVE_EACH_MS )
        return;

    // We're ready now, enough time has passed. 
    mMsAccumulator -= SNAKE_MOVE_EACH_MS;

    // dir contains the last valid direction, it's safe to use.
    // The player will be able to change it until we're ready to update
    // the position of the snake. 
    if (dir == DOWN ) {
        head = new Point(head.getX(), head.getY() + SNAKE_SPEED_FRAME_PX);
        mLastDir = dir;
    }
    if (dir == UP) {
        head = new Point(head.getX(), head.getY() - SNAKE_SPEED_FRAME_PX);
        mLastDir = dir;
    }
    if (dir == RIGHT) {
        head = new Point(head.getX() + SNAKE_SPEED_FRAME_PX, head.getY());
        mLastDir = dir;
    }
    if (dir == LEFT) {
        head = new Point(head.getX() - SNAKE_SPEED_FRAME_PX, head.getY());
        mLastDir = dir;
    }       

}


public void keyPressed (int key, char c) {

    if ((key == Input.KEY_LEFT) && mLastDir != RIGHT) {
        dir = LEFT; 
    }
    if ((key == Input.KEY_RIGHT) && mLastDir != LEFT) {
        dir = RIGHT; 
    }
    if ((key == Input.KEY_UP) && mLastDir != DOWN) {
        dir = UP; 
    } 
    if ((key == Input.KEY_DOWN) && mLastDir != UP) {
        dir = DOWN; 
    }

}
\$\endgroup\$
  • \$\begingroup\$ I do quite like this approach, allows the ability to hold an input direction until the snake has moved. +1 \$\endgroup\$ – Tom 'Blue' Piddock Sep 17 '15 at 15:54
  • \$\begingroup\$ @Alexandre, I implemented your approach and It works, though I'm still trying to understand. I do appreciate your respond and generousness. Would you mind if I ask for your time for a few more minutes considering link this code. I'd implemented it before I got to know Slick. The point is this code allow the key to be pressed only once per update regardless the speed of game. I have tried to apply same logic to the Slick approach i.e the one that we discussed above, but It's failed. I have comment the word POINT for you to emphasize. \$\endgroup\$ – NERDYLIZARD Sep 17 '15 at 16:19
  • \$\begingroup\$ @NERDYLIZARD From what I understand, the difference between what's in the link you posted and the question you asked is exactly what's being underlined by @Blue. You need to flag that the direction has already been set and cannot be set another time during the frame: directionSet in @Blue's code, and keyPressed in the code in the link. You do not have this mechanism in place in the code you posted in the OP. \$\endgroup\$ – Vaillancourt Sep 17 '15 at 16:53
  • \$\begingroup\$ Further discussions should be done in this chat room \$\endgroup\$ – Vaillancourt Sep 17 '15 at 17:07
  • \$\begingroup\$ @Alexandre Could you manifest this line "You do not have this mechanism in place in the code you posted in the OP". I've tried 'keyPressed' ('directionSet') approach to both codes. But I wonder why it doesn't work for the one implemented by Slick(the question one). \$\endgroup\$ – NERDYLIZARD Sep 17 '15 at 17:09
8
\$\begingroup\$

Firstly, you need to use a switch case statement and decide which direction overrides the others, for example, if they press all four buttons, which button should be listened to for input? This gets put in order within the switch statements.

Edit: For clarity about the above statement. You do need to use a switch case statement (or similar structural logic) to stop the following from happening.

dir = up
inputs == down left

if dir != right & input.has(left):
    dir = left

if dir != up & input.has(down):
   dir = down

# dir started as up but is now down, an illegal move in Snake

Original intent is to go from moving up, to moving left and not down (as this causes the snake to fold back on it's body and die. Instead your logic makes you move left, and then down before your input logic finishes. This is why having the if/else or switch/case statement is necessary.

You can now use a boolean field in the class to keep a track of the input being successfully processed, and update it each time. You can have different bools for different types of inputs, not just directions. For example, you can track if "Pause" has been pressed so you don't accidentally unpause in the same update loop, or if you had a "Run" / "Slow" button but you want Run to have a bigger priority.

// This boolean is used to show if an input has been accepted this update.
private bool directionSet = false;

public void update(GameContainer gc, StateBasedGame sbg, int delta) throws SlickException 
{
    switch(dir)
    {
        case DOWN:
            head = new Point(head.getX(), head.getY() + SPEED * delta);
            break;
        case UP:
            head = new Point(head.getX(), head.getY() - SPEED * delta);
            break;
        case RIGHT:
            head = new Point(head.getX() + SPEED * delta, head.getY());
            break;
        case LEFT:
            head = new Point(head.getX() - SPEED * delta, head.getY());
            break;
    }

   directionSet = false;
}

public void keyPressed (int key, char c) 
{
    // You can still set more input logic here or at the bottom of the method:
    // ...

    // Check if a direction is set before accepting new directional input
    if(!directionSet)
    {
        switch(key)
        {
            case Input.KEY_LEFT:
                if(dir != RIGHT)
                {
                    dir = LEFT; 
                    directionSet = true; // Set on each input
                }
                break;
            case Input.KEY_RIGHT:
                if(dir != LEFT)
                {
                    dir = RIGHT; 
                    directionSet = true; 
                }
                break;
            case Input.KEY_UP:
                if(dir != DOWN)
                {
                    dir = UP;  
                    directionSet = true;
                }
                break;
            case Input.KEY_DOWN:
                if(dir != UP)
                {
                    dir = DOWN; 
                    directionSet = true;
                }
                break;        
        }
    }
}
\$\endgroup\$
  • \$\begingroup\$ I appreciate your answer and it seems to work when I was reading. After I finished implementing, the problem remains. Simultaneous press is still allowed and the logical error comes along with it is that when 2 keys are pressed at the same time eg. {up, left} when direction is 'right'. The result is direction changes to up, so the following 'left' key can be pressed. Consequently, the direction turns to Left before snake updating itself that means snake will turn backward and eat up his own tail. \$\endgroup\$ – NERDYLIZARD Sep 17 '15 at 11:37
  • 2
    \$\begingroup\$ @NERDYLIZARD Then you probably need to block the input until the snake has actually moved one tile? The suggested algorithm forbids of having more than one input per frame, so your issue is with the time the snake takes before being able to take the 'next input'. If your game is running at 30 fps, and your snake is traveling at 1 tile per second, you'll have to poll your input only once per second to know where the snake will go next. \$\endgroup\$ – Vaillancourt Sep 17 '15 at 11:48
  • \$\begingroup\$ @AlexandreVaillancourt could you manifest this statement "If your game is running at 30 fps, and your snake is traveling at 1 tile per second". I mean how to calculate the distance that snake can travel per frame. \$\endgroup\$ – NERDYLIZARD Sep 17 '15 at 11:56
  • 3
    \$\begingroup\$ Well if you take a look at this, you'll see that the snake moves at about 2 tiles per second, and presumably, the frame rate is 2 FPS. Now I'm not sure how you implemented your game loop, but the delta should be 1 and the snake should move 1 tile per frame, so no 'speed' needed. Now if you want to smooth the transition from tile 1 to tile 2, you have to decouple the visual position and animation of the snake from it's logical position. Basically, the snake can't change orientation until it's ready for it, and this can't be 30 times per second. \$\endgroup\$ – Vaillancourt Sep 17 '15 at 12:23
  • 1
    \$\begingroup\$ Please don't use comments for extended debate about minor details that don't really matter in the broader sense of the question; comments are not for discussion, there's a Game Development Chat for that. \$\endgroup\$ – Josh Sep 17 '15 at 15:59
1
\$\begingroup\$

I've also developed snake, and ran into the same problem. I run snake at 30fps, yet only 'update' every 10 frames, in between the player still has the freedom to change direction.

You're trying to prevent a player from going back where he came from in the input handling. Say you were moving up, your input handling will prevent the player from moving down, but doing {right, down} as you described will be two seperate, valid movements in one update cycle as it were.

I solved this by storing the direction a player moved in in the previous update, and use this to prevent the opposite direction from being a legal move.

EDIT: In your movement method you need to keep track of your previous movements. Say you've decided to move UP this cycle (based on the user's input) then the actual movement code would look like

private void move(int dir) {
  switch(dir) {
    //your own abstraction of the directions, you could use strings as well
    case DIRS.UP: player.y -= 10; previousMovement = DIRS.UP;
    ..
  }
}

The player has now moved, and stored the direction in which it has moved. Now all that's left do to in the input handling is to check for this variable.

private void handleInput(int key) {
  if ((key == Input.KEY_DOWN && previousMovement != DIRS.UP)) {
    directionToMoveNextUpdate = DIRS.DOWN;
  }
}

The clue is that you do not check based on your previous input but based on your previous movement before setting a new direction. previousMovement is only changed when the player moves, you can press as many buttons as you want during each update cycle and it wouldn't affect it.

I hope that clears it up, if not: look at my github (ThommyTheThird) for a working AS3 example.

\$\endgroup\$
  • \$\begingroup\$ that's exactly my problem, could you clarify a little bit more about how you fixed this bug? \$\endgroup\$ – NERDYLIZARD Sep 18 '15 at 11:17
  • 1
    \$\begingroup\$ I've updated my answer to provide a more detailed step-by-step explanation, hopefully clearing it up. \$\endgroup\$ – Thomas Withaar Sep 25 '15 at 16:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.