Jump to content
IGNORED

Help with moving sprite through playfield with obstacles


DZ-Jay

Recommended Posts

I'm working on a routine that moves a sprite across a play-field filled with obstacles.  The sprite moves in 16 directions, using a table of pre-computed velocity vectors for each direction; and will move on an axis unless an obstacle (i.e., a background card which is not one of the "walkable path" set) is encountered.

 

The problem is that I believe my algorithm is very inefficient and would like some help in improving it, if possible.  Below I describe the algorithm.

 

I test the displacement on each axis independently, so that the sprite can continue moving on any axis which is not directly blocked.  This allows the sprite to slide across the edge of obstacles when its direction is not strictly perpendicular to it -- that is to say, when encountering the obstacle at an angle.  This is illustrated in the diagram below.

image.png.9b8f9a8336d107d528f9111c7dc313ae.png

In the above illustration, the sprite is moving in a 45˚ angle to the right (direction #2 of 16).  We test the displacement on the X axis (right) and discover an obstacle there.  We also test the displacement on the Y axis (up) and notice that the path is open.  Consequently, the sprite slides across the edge of the obstacle upwards.

 

Below is some pseudo-code describing the tests.  Notice that we adjust the positions by 4 pixels on each axis, to check the card directly under the center of the sprite (origin + 4).

' (PosX, PosY) are the current position
' (TempPosX, TempPosY) are the target positions after displacement
TempPosX = PosX + VelX
TempPosY = PosY + VelY

' Test displacement in X:
'   Check if the card directly underneath the
'   new position is open or blocked.
If PathIsOpen(TempPosX + 4, PosY + 4) Then
    PosX = TempPosX
End If

' Test displacement in Y:
'   Check if the card directly underneath the
'   new position is open or blocked.
If PathIsOpen(PosX, TempPosY + 4) Then
    PosY = TempPosY
End If

 

 

This works rather well, but it makes the play-field feel a little blocky, since the sliding movement is constrained to right-angles.  So, I allocated a few cards in GRAM for special "corner" blocks.  The idea is that when the sprite is over one of these special cards, it can slide across a diagonal edge on the card.  There are four of these special cards, one for each of the four corners:

 0 .......#    #.......
 1 ......##    ##......
 2 .....###    ###.....
 3 ....####    ####....
 4 ...#####    #####...
 5 ..######    ######..
 6 .#######    #######.
 7 ########    ########
   01234567    01234567

 0 ######## 0  ########
 1 .####### 1  #######.
 2 ..###### 2  ######..
 3 ...##### 3  #####...
 4 ....#### 4  ####....
 5 .....### 5  ###.....
 6 ......## 6  ##......
 7 .......# 7  #.......
   01234567    01234567

 

Because the slope of the edges are purely diagonal, dissecting the cards down the middle, the maximum offset in one axis can be derived directly from the current offset in the other.

 

For example, consider the first case, the top-left corner:


    ; Threshold   ------,
    ; Axis Offset --,   |
    ;               |   |
    ; 0             Y   X <         X   Y <
    .......# 0      0   7           0   7
    ......## 1      1   6           1   6
    .....### 2      2   5           2   5
    ....#### 3      3   4           3   4
    ...##### 4      4   3           4   3
    ..###### 5      5   2           5   2
    .####### 6      6   1           6   1
    ######## 7      7   0           7   0

 

When moving in the X direction, we compare the vertical offset within the card to know how much the sprite is allowed to move in the current axis.  When the Y offset is 1, the maximum X offset is 6, and so on.  In this particular case, the maximum X offset is the complement of the current Y offset; i.e., MaxOffsetX = (8 - OffsetY).

 

Below is some pseudo code describing the algorithm.

    ' (PosX, PosY) are the current position
    ' (TempPosX, TempPosY) are the target positions after displacement
    TempPosX = PosX + VelX
    TempPosY = PosY + VelY

    OffsetX = TempPosX % 8
    OffsetY = TempPosY % 8

    ' The sprite can move in X up to an offset
    ' that is the complement of the Y offset.
    '
    ' In other words, when the Y offset is 1,
    ' the sprite can move up to offset 7.
    If (OffsetX <= (8 - OffsetY)) Then
      ' Sprite can move ...
      PosX = TempPosX
    Else
      ' Sprite is blocked, cap it to the threshold ...
      PosX = (8 - OffsetY)
    End If

 

The offsets and comparisons change depending on the axis and the card -- there are four cases for each of the two axis, so eight comparisons in total.

 

This all works rather well, as illustrated in the video below:

 

I'm quite happy with the way it works -- it's just that I'm afraid the code is horrendously inefficient. 😬

 

I admit that my algorithm is not the best.  It is probably quite naïve, and I'm sure there are better ways to do the same thing.  First off, I operate on each axis individually, which leads to the same sort of computations being done twice.

 

Below is the full algorithm, taken straight from my code and modified slightly to generalize the variables, remove any irrelevant parts, and make it easier to follow.

Spoiler
'' ======================================================================== ''
''  Input:
''      Sprite velocity and position are in Q8.8 fractional values.
''
''      #VelocityX      Velocity X component (Q8.8 fraction)
''      #VelocityY      Velocity Y component (Q8.8 fraction)
''      #PositionX      Current sprite position in X (Q8.8 fraction)
''      #PositionY      Current sprite position in Y (Q8.8 fraction)
''
''  Temp:
''      TempX           Temporary X position, (Q8 integer)
''      TempY           Temporary Y position, (Q8 integer)
''      TempCard        Index in GRAM of card directly under sprite
''      TempMaxOffset   Maximum offset within 8x8 background card
''
''      OffsetX         X Offset of sprite within current 8x8 background card
''      OffsetY         Y Offset of sprite within current 8x8 background card
''      #TempDist
''
'' ======================================================================== ''
MoveObjectInPath: Procedure
    ' ----------------------------------------------------------------
    ' Compute new X position
    ' ----------------------------------------------------------------
    If (#VelocityX <> 0) Then
        #TempPosX = (#VelocityX + #PositionX)
        TempX     = (#TempPosX / 256)

        ' Constrain the X position within the screen horizontal boundaries
        If (TempX <= PLAYFIELD_MIN_X) Then
            #PositionX = (PLAYFIELD_MIN_X * 256)
        ElseIf (TempX >= PLAYFIELD_MAX_X) Then
            #PositionX = (PLAYFIELD_MAX_X * 256)
        Else
            ' Constraint the X position to the open path.
            ' --------------------------------------------------------

            ' We'll use the current Y position to isolate changes in X.
            ' In other words, we are testing the card directly underneath
            ' the sprite after displacing it only in X direction.
            TempY    = (#PositionY / 256)
            TempCard = GetBackgroundCardIndex(TempX, TempY)

            ' Check if path is open ...
            If CheckPathIsOpen(TempCard) Then
                ' Path is open, so adjust the position
                #PositionX = #TempPosX

            ' Check if it is a sloped-path ...
            ElseIf CheckPathIsSloped(TempCard) Then
                ' Compute X and Y offsets within 8x8 background card.
                ' The offset is computed from the center of the sprite,
                ' which is 4 pixels from origin, e.g., (TempX + 4).
                OffsetX = ((TempX + 4) % 8)
                OffsetY = ((TempY + 4) % 8)

                ' Determine the X offset and compute the distance to
                ' maximum threshold.  The sign of the distance tells us
                ' whether it is before (positive) or beyond (negative)
                ' the threshold.
                On (TempCard And 3) Fast Goto \
                                    .SlopeX0, \
                                    .SlopeX1, \
                                    .SlopeX2, \
                                    .SlopeX3

                     ' Top-Left
                     '      .......# 0
                     '      ......## 1
                     '      .....### 2
                     '      ....#### 3
                     '      ...##### 4
                     '      ..###### 5
                     '      .####### 6
                     '      ######## 7  Threshold = (8 - #OffsetY)
                     '      01234567
                    .SlopeX0:
                        #TempDist     = ((8 - OffsetY) - OffsetX)
                        TempMaxOffset = #TempDist
                        Goto .EndCaseX

                    ' Bottom-Left
                    '       ######## 0
                    '       .####### 1
                    '       ..###### 2
                    '       ...##### 3
                    '       ....#### 4
                    '       .....### 5
                    '       ......## 6
                    '       .......# 7  Threshold = OffsetY
                    '       01234567
                    .SlopeX1:
                        #TempDist     = (OffsetY - OffsetX)
                        TempMaxOffset = #TempDist
                        Goto .EndCaseX

                    ' Top-Right
                    '       #....... 0
                    '       ##...... 1
                    '       ###..... 2
                    '       ####.... 3
                    '       #####... 4
                    '       ######.. 5
                    '       #######. 6
                    '       ######## 7  Threshold = OffsetY
                    '       01234567
                    .SlopeX2:
                        #TempDist     = (OffsetX - OffsetY)
                        TempMaxOffset = -(#TempDist)
                        Goto .EndCaseX

                    ' Bottom-Right
                    '       ######## 0
                    '       #######. 1
                    '       ######.. 2
                    '       #####... 3
                    '       ####.... 4
                    '       ###..... 5
                    '       ##...... 6
                    '       #....... 7  Threshold = (8 - OffsetY)
                    '       01234567
                    .SlopeX3:
                        #TempDist     = (OffsetX - (8 - OffsetY))
                        TempMaxOffset = -(#TempDist)
                        ' Goto .EndCaseX

                .EndCaseX:

                ' When the distance is negative, it means the sprite
                ' has gone beyond the sloped boundary; so we adjust
                ' the position accordingly.
                If (#TempDist < 0) Then
                    TempX = (TempX + TempMaxOffset)
                    #TempPosX   = (TempX * 256)
                End If

                ' Update the X position
                #PositionX = #TempPosX

            ' Otherwise, path is blocked ...
            Else
                ' The offset is computed from the center of the sprite
                ' which is 4 pixesl from its origin.
                OffsetX = ((TempX + 4) % 8)

                ' When the path is blocked, we need to adjust the
                ' position in the opposite direction to that in which
                ' the sprite is moving.
                '
                ' In other words, if the sprite is moving left when
                ' encountering an obstacle, we assume that the obstacle
                ' is on the left side, so we adjust the position to the
                ' right.
                
                ' The sign of the velocity indicates the direction in X
                ' (negative = left, positive = right).
                If (#VelocityX < 0) Then
                    TempX = (TempX + (8 - OffsetX))
                Else
                    TempX = (TempX - (OffsetX + 1))
                End If

                ' Update the X position
                #PositionX = (TempX * 256)

            End If

        End If
    End If

    ' ----------------------------------------------------------------
    ' Compute new Y position
    ' ----------------------------------------------------------------
    If (#VelocityY <> 0) Then
        #TempPosY = (#VelocityY + #PositionY)
        TempY     = (#TempPosY / 256)

        ' Constrain the Y position within the screen vertical boundaries
        If (TempY <= PLAYFIELD_MIN_Y) Then
            #PositionY = (PLAYFIELD_MIN_Y * 256)
        ElseIf (TempY >= PLAYFIELD_MAX_Y) Then
            #PositionY = (PLAYFIELD_MAX_Y * 256)
        Else
            ' Constraint the Y position to the open path
            ' --------------------------------------------------------

            ' We'll use the current X position to isolate changes in Y.
            ' In other words, we are testing the card directly underneath
            ' the sprite after displacing it only in Y direction.
            TempX   = (#PositionX / 256)
            TempCard = GetBackgroundCardIndex(TempX, TempY)

            ' Check if the path is blocked ...
            If CheckPathIsOpen(TempCard) Then
                #PositionY = #TempPosY

            ' Check if it is a sloped-path ...
            ElseIf CheckPathIsSloped(TempCard) Then
                ' Compute X and Y offsets within 8x8 background card
                ' The offset is computed from the center of the sprite,
                ' which is 4 pixels from origin, e.g., (TempY + 4).
                OffsetX = ((TempX + 4) % 8)
                OffsetY = ((TempY + 4) % 8)

                ' Determine the Y offset and compute the distance to
                ' maximum threshold.  The sign of the distance tells us
                ' whether it is before (positive) or beyond (negative)
                ' the threshold.
                On (TempCard And 3) Fast Goto \
                                    .SlopeY0, \
                                    .SlopeY1, \
                                    .SlopeY2, \
                                    .SlopeY3

                     ' Top-Left
                     '      .......# 0
                     '      ......## 1
                     '      .....### 2
                     '      ....#### 3
                     '      ...##### 4
                     '      ..###### 5
                     '      .####### 6
                     '      ######## 7  Threshold = (8 - OffsetY)
                     '      01234567
                    .SlopeY0:
                        #TempDist     = ((8 - OffsetX) - OffsetY)
                        TempMaxOffset = #TempDist
                        Goto .EndCaseY

                    ' Bottom-Left
                    '       ######## 0
                    '       .####### 1
                    '       ..###### 2
                    '       ...##### 3
                    '       ....#### 4
                    '       .....### 5
                    '       ......## 6
                    '       .......# 7  Threshold = OffsetX
                    '       01234567
                    .SlopeY1:
                        #TempDist     = (OffsetY - OffsetX)
                        TempMaxOffset = -(#TempDist)
                        Goto .EndCaseY

                    ' Top-Right
                    '       #....... 0
                    '       ##...... 1
                    '       ###..... 2
                    '       ####.... 3
                    '       #####... 4
                    '       ######.. 5
                    '       #######. 6
                    '       ######## 7  Threshold = OffsetX
                    '       01234567
                    .SlopeY2:
                        #TempDist     = (OffsetX - OffsetY)
                        TempMaxOffset = #TempDist
                        Goto .EndCaseY

                    ' Bottom-Right
                    '       ######## 0
                    '       #######. 1
                    '       ######.. 2
                    '       #####... 3
                    '       ####.... 4
                    '       ###..... 5
                    '       ##...... 6
                    '       #....... 7  Threshold = (8 - OffsetX)
                    '       01234567
                    .SlopeY3:
                        #TempDist     = (OffsetY - (8 - OffsetX))
                        TempMaxOffset = -(#TempDist)
                        ' Goto .EndCaseY

                .EndCaseY:

                ' When the distance is negative, it means the sprite
                ' has gone beyond the sloped boundary; so we adjust
                ' the position accordingly.
                If (#TempDist < 0) Then
                    TempY = (TempY + TempMaxOffset)
                    #TempPosY   = (TempY * 256)
                End If

                ' Updated the Y position
                #PositionY = #TempPosY

            ' Otherwise, path is blocked ...
            Else
                ' The offset is computed from the center of the sprite
                ' which is 4 pixesl from its origin.
                OffsetY = (TempY % 8)

                ' When the path is blocked, we need to adjust the
                ' position in the opposite direction to that in which
                ' the sprite is moving.
                '
                ' In other words, if the sprite is moving downwards when
                ' encountering an obstacle, we assume that the obstacle
                ' is below, so we adjust the position upwards.

                ' The sign of the velocity indicates the direction in Y
                ' (negative = up, positive = down).
                If (#VelocityY < 0) Then
                    TempY = (TempY + (8 - OffsetY))
                Else
                    TempY = (TempY - (OffsetY + 1))
                End If

                ' Updated the Y position
                #PositionY = (TempY * 256)
            End If

        End If
    End If

    Return
End

 


As you can see, each axis is processed separately, and includes three main checks:

  • Is the sprite new position outside bounds?  Adjust position accordingly to keep it within bounds.
  • Is the sprite new position on an open path card?  Accept new position
  • Is the sprite new position on a sloped path card?  Adjust the position depending on which card it is, and its offset within it.
  • Is the sprite new position on a blocked card?  Adjust the position depending on whether the velocity is positive or negative.

 

I would imagine that this is a solved problem and that there are easier and simpler ways to attack it.  I'm sure to all you veterans out there, the above is shockingly, incredibly, and stupidly more complicated than it needs to be.  But that's as best as my little brain can figure out.  :dunce:

 

Any help will be greatly appreciated!

 

     -dZ.

  • Like 2
Link to comment
Share on other sites

With my Towering Inferno game. This was my first time using top down 16 directional movement. So my solution was to restore the previous player position if they collide with the wall(impassable object), which doesn't allow them to slide against the wall, thus making it sticky. 

Link to comment
Share on other sites

1 hour ago, Kiwi said:

With my Towering Inferno game. This was my first time using top down 16 directional movement. So my solution was to restore the previous player position if they collide with the wall(impassable object), which doesn't allow them to slide against the wall, thus making it sticky. 


Thanks.  I quite like the sliding -- it's a feature I built by design, not a side-effect.  That's the reason I test each axis independently, so that I can isolate displacement on on axis and adjust it if blocked, or allow it if not.

 

    dZ.

Link to comment
Share on other sites

By the way, the "On (TempIdx And 3) Fast Goto ..." structure above is my IntyBASIC hacky way of implementing a switch statement.

 

If @nanochess is paying attention, I wished IntyBASIC had a switch statement. ;)

 

It could follow the Visual Basic "Select Case" statement, but work as a dispatch table like in my example above.  I do not mind using this pattern at all, but it feels silly to have to include the Goto table and the "Goto <end label>," and having to come up with unique custom labels for each case that do not collide with any others in the same scope. *sigh*

 

So, this:

On (TempIdx And 3) Fast Goto \
                    .SlopeX0, \
                    .SlopeX1, \
                    .SlopeX2, \
                    .SlopeX3

    .SlopeX0:
        #TempDist  = ((MOB_AXIS_LENGTH - #DeltaY) - #DeltaX)
        TempOffset = #TempDist
        Goto .EndCaseX

    .SlopeX1:
        #TempDist  = (#DeltaY - #DeltaX)
        TempOffset = #TempDist
        Goto .EndCaseX

    .SlopeX2:
        #TempDist  = (#DeltaX - #DeltaY)
        TempOffset = -(#TempDist)
        Goto .EndCaseX

    .SlopeX3:
        #TempDist  = (#DeltaX - (MOB_AXIS_LENGTH - #DeltaY))
        TempOffset = -(#TempDist)
        ' Goto .EndCaseX

.EndCaseX:

 

Could be done similar to something like this:

Select Case (TempIdx And 3)
    Case 0
        #TempDist  = ((MOB_AXIS_LENGTH - #DeltaY) - #DeltaX)
        TempOffset = #TempDist

    Case 1
        #TempDist  = (#DeltaY - #DeltaX)
        TempOffset = #TempDist

    Case 2
        #TempDist  = (#DeltaX - #DeltaY)
        TempOffset = -(#TempDist)

    Case 3
        #TempDist  = (#DeltaX - (MOB_AXIS_LENGTH - #DeltaY))
        TempOffset = -(#TempDist)

End Select

 

And the compiler would create the labels, the jump table, and the breakout GOTOs, automatically.

 

     -dZ.

  • Like 1
Link to comment
Share on other sites

11 minutes ago, nanochess said:

I don't see your code as inefficient.

 

In fact my programmer mind sees it as the minimum code possible, as each direction must be computed independently otherwise you cannot get the slide feature.

 

 

Hi, @nanochess, thanks for responding.  I appreciate you taking the time to look at the code.  I sort of thought that doing each axis separately was the only way to get the slide feature, but I wasn't really sure.  (Actually, I tried adjusting the position on both axes at once, and re-arranging the tests, but I couldn't get it to work correctly, so I thought I could be doing it wrong.)

 

Still, you or anybody can provide some feedback on how to improve it, it would be greatly appreciated.  Maybe the computations or the tests conditions could be improved?

 

    -dZ.

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...