Moving along, enemy movement using a route.

5-28-2018

Today in the USA is Memorial Day, a day in which we honor those in the Military that died while serving. It has its roots in the American Civil War. Happy Memorial Day to everyone that is reading this.

Back to programming then.

Now that we have created our route using the Grassfire technique, now its time to move the enemy sprites along the pathway we have created for them. Our prior routine for moving the enemy is now obsolete, so time to get rid of it to make way for the new routine. I actually never truly delete a routine in my code, I just move it to another text file in another directory and save it, for when I need to look at a concept again. I think the routine was called enemyCollision, I had actually gotten rid of it ages ago as I have been concentrating on the Grassfire technique for a while.

This is more of the icing on top of the cake, we know our pathway since we incrementally create it during the Grassfire method. Now we need to make sure our enemy travels this path to find our player. This allows the enemy to give chase to you so that you don’t have time to sit still too long before the enemy is upon you.

So, let’s create our function.

void moveEnemy(unsigned char enemyXX, unsigned char enemyYY, unsigned char enemyFF)
{
    enemySteps = enemyFF << 4;// Strip enemy steps out of enemyFF
    enemySteps = enemySteps >> 4;// adjust variable to the correct range
    enemyDirection = enemyFF << 1;// Strip enemy direction out of enemyFF
    enemyDirection = enemyDirection >> 5;// adjust variable to the correct range
    enemyRetreat = enemyFF >> 7;// Strip enemy retreat out of enemyFF

    //adjust enemy to character placement from pixel placement
    enemyCharX = enemyXX >> 4;//enemyCharX character range 0 - 14
    enemyCharY = enemyYY >> 4;//enemyCharY character range 0 - 9

    center = enemyCharX + scrw * enemyCharY;
    north = enemyCharX + scrw * (enemyCharY - 1);
    south = enemyCharX + scrw * (enemyCharY + 1);
    east = enemyCharX + 1 + scrw * enemyCharY;
    west = enemyCharX - 1 + scrw * enemyCharY;
    
    //try to keep the enemies from going into barriers
    if (tiles[north] > 0)
    {
        grassfire[north] = 255;
    }
    if (tiles[south] > 0)
    {
        grassfire[south] = 255;
    }
    if (tiles[east] > 0)
    {
        grassfire[east] = 255;
    }
    if (tiles[west] > 0)
    {
        grassfire[west] = 255;
    }
    
    //CenterofTile = (half_tileSize + (tileSize * tileNumber))	
    xCenterofTile = (8+ (16 * enemyCharX));
    yCenterofTile = (8+ (16 * enemyCharY));    
    //if the enemy is in the center of the tile, need to look which direction we need to travel
    if ((enemyXX == xCenterofTile) && (enemyYY == yCenterofTile))
    {        
        centerValue = grassfire[center];
        northValue = grassfire[north];
        southValue = grassfire[south];
        eastValue = grassfire[east];
        westValue = grassfire[west];
        
        //decide which way the enemy should travel
        if ((northValue < southValue && northValue < eastValue && northValue < westValue))
        {
            //go north
            enemyDirection = 1;
        }
        else if    ((southValue < northValue && southValue < eastValue && southValue < westValue))
        {
            //go south
            enemyDirection = 2;
        }
        else if    ((eastValue < northValue && eastValue < southValue && eastValue < westValue))
        {
            //go east
            enemyDirection = 3;
        }
        
        else if ((westValue < northValue && westValue < southValue && westValue  232)
        {
            enemyX--;
            enemyDirection = 4;//need to go west
        }
        if (enemyX < 8)
        {
            enemyX++;
            enemyDirection = 3;//need to go east
        }
        if (enemyY  152)
        {
            enemyY--;
            enemyDirection = 1;//need to go north
        }
    }    
    {
        if (enemyDirection == 1)//go north/up
        {
            enemyY--;
        }
        if (enemyDirection == 2)//go south/down
        {
            enemyY++;
        }
        if (enemyDirection == 3)//go east/right
        {
            enemyX++;
        }    
        if (enemyDirection == 4)//go west/left
        {
            enemyX--;
        }
    }

    enemyDirection = enemyDirection << 4;
    enemyRetreat = enemyRetreat << 7;
    enemyF = enemySteps | enemyDirection | enemyRetreat;    
}

You may notice that I did not declare any new variables within the function itself, rather any new variables are declared in variables.h.

Let’s start with the commands.


    enemySteps = enemyFF << 4;// Strip enemy steps out of enemyFF
    enemySteps = enemySteps >> 4;// adjust variable to the correct range
    enemyDirection = enemyFF << 1;// Strip enemy direction out of enemyFF
    enemyDirection = enemyDirection >> 5;// adjust variable to the correct range
    enemyRetreat = enemyFF >> 7;// Strip enemy retreat out of enemyFF

In actuality, we are not using the enemySteps and enemyRetreat variables any longer, however, I am still using the enemyDirection variable to store the direction our enemy will travel in. Rather than rewrite the neat-o shifting we wrote at some other point, I decided to leave it in, for now. I could still store information in there, so it will lie unused until I decided to reuse it again.

So in short, we are still using enemyDirection in that bit of code.

//adjust enemy to character placement from pixel placement
enemyCharX = enemyXX >> 4;//enemyCharX character range 0 - 14
enemyCharY = enemyYY >> 4;//enemyCharY character range 0 - 9

Here, I am adjusting the enemy placement from the 0-240 (on the X-axis) and 0-160(on the Y-axis) to the tile size of 0-14 (on the X-axis) and 0-9 (on the Y-axis).

We’re doing this to look where the obstacles are as well as the pathway, so our enemy can make an intelligent choice on where to travel to next.

Our next section of code is this.

center = enemyCharX + scrw * enemyCharY;
north = enemyCharX + scrw * (enemyCharY - 1);
south = enemyCharX + scrw * (enemyCharY + 1);
east = enemyCharX + 1 + scrw * enemyCharY;
west = enemyCharX - 1 + scrw * enemyCharY;

This is our indexing to be able to allow our enemy to look not only at the tile space it is currently on, but also be able to look to the north (up), south (down), east (right) and west (left). The formula is based on the tile size and just looks to the tile square adjacent to the enemy’s current position.

//try to keep the enemies from going into barriers
if (tiles[north] > 0)
{
    grassfire[north] = 255;
}
if (tiles[south] > 0)
{
    grassfire[south] = 255;
}
if (tiles[east] > 0)
{
    grassfire[east] = 255;
}
if (tiles[west] > 0)
{
    grassfire[west] = 255;
}

These next set of commands may seem a little confusing since we have already found our obstacles during the setup phase of our Grassfire. I am including it here “just in case” this will double check to make sure that the grassfire still shows the enemy where the obstacles are. It will also serve another function, it will look to see if there are any sudden obstacles. A sudden obstacle could be any immediate pop-up obstacle that we might place in. A sudden obstacle could be a bonus item or an obstacle that may come up with a trigger of some sort. It can’t hurt to have it in.

//CenterofTile = (half_tileSize + (tileSize * tileNumber))	
xCenterofTile = (8+ (16 * enemyCharX));
yCenterofTile = (8+ (16 * enemyCharY));

Here are another few commands that might confuse you, we have to make sure that our enemy is centered on a tile, otherwise, our enemy will eventually go askew. We will need to know the X Center as well as the Y Center.

We also need to make sure that the enemy is centered on the tile while starting up. That is found where we initialize the enemy sprite setting, found in mapsandstart.h

if (Level == 0)
{
    //enemy 1 - 8, 8, 8, 3
    data[4] = 8;//n =sprite number
    data[5] = 24;//x position
    data[6] = 24;//y position
    data[7] = 0;//f
……

Back to the center of the tile, as you can see, the tile center is fairly easy to find, our tile size is 16 pixels. The formula is similar to the index formula that we use to find a tile in an array.

//if the enemy is in the center of the tile, need to look which direction we need to travel
    if ((enemyXX == xCenterofTile) && (enemyYY == yCenterofTile))

Here, we look to see if the enemy is in the center of the tile, if it is, then we are going to proceed with our evaluation.

//if the enemy is in the center of the tile, need to look which direction we need to travel
if ((enemyXX == xCenterofTile) && (enemyYY == yCenterofTile))
{        
    centerValue = grassfire[center];
    northValue = grassfire[north];
    southValue = grassfire[south];
    eastValue = grassfire[east];
    westValue = grassfire[west];
        
    //decide which way the enemy should travel
    if ((northValue < southValue && northValue < eastValue && northValue < westValue))
    {
        //go north
        enemyDirection = 1;
    }
    else if    ((southValue < northValue && southValue < eastValue && southValue < westValue))
    {
        //go south
        enemyDirection = 2;
    }
    else if    ((eastValue < northValue && eastValue < southValue && eastValue < westValue))
    {
        //go east
         enemyDirection = 3;
    }        
    else if ((westValue < northValue && westValue < southValue && westValue < eastValue))
    {
        //go west
        enemyDirection = 4;
    }        
    //decide which way the enemy should travel
}

Here we are creating our decision on which way to travel (once the criteria for the center of the tile is met). We do so by looking in each of the cardinal directions (north, south, east, and west) and storing the tile path number laid out in the Grassfire method. Next, we look to see which of the 4 directions have the lowest value. On each of the cardinal directions, we have a course we can set for our enemy to take.

Once the direction is set, we will not look at it again, until we reach a center tile condition and then our course will be reevaluated. This is like a waypoint, once we reach a waypoint, look to see where the best waypoint is and then travel in that direction.

The next section keeps our enemies within the borders of the playing area.

    //keep enemies from going past border
    //need hard numbers
    {
        if (enemyX > 232)
        {
            enemyX--;
            enemyDirection = 4;//need to go west
        }
        if (enemyX < 8)
        {
            enemyX++;
            enemyDirection = 3;//need to go east
        }
        if (enemyY  152)
        {
            enemyY--;
            enemyDirection = 1;//need to go north
        }
    }

If the enemy gets to the edge, either less than 8 or more than 232 on the X-axis or less than 8 or more than 152 in the Y-axis, we push the enemy in the opposite direction and set a direction for the enemy to traverse to.

Our final set of commands actually move our enemy sprite.

    {
        if (enemyDirection == 1)//go north/up
        {
            enemyY--;
        }
        if (enemyDirection == 2)//go south/down
        {
            enemyY++;
        }
        if (enemyDirection == 3)//go east/right
        {
            enemyX++;
        }    
        if (enemyDirection == 4)//go west/left
        {
            enemyX--;
        }
    }

This is the section that follows the direction when we plot our course, moving pixel by pixel until we make another decision.

So, our large overview of the function is that we only plot our course when we hit the center of the tile and once the center conditions are met, we set a direction. We will continue in that direction via enemyX or enemyY until we hit the center again and plot our next waypoint. Pretty simple.

However, there seem to be a few problems with the entire function once the game is compiled.

The first problem I encounter is that if you remain still, the enemies would move to your tile just fine, however, if you moved, the enemy would still go to your old spot or stop at a spot on the trail that you made. Frustrated by that, I checked my routine again, and then finally checked the array. I found I was leaving a trail of 1’s across the board on all tiles I had been to. I was not updating my array with my movement.

The solution is found in the player movement, which is found in movement.h, in the function moveMainCharacter().

I placed the following commands near the bottom of the function.

    //refresh the tiles on moving
    if (tiles[north] == 0)
        grassfire[north] = 2;
    else
        grassfire[north] = 255;
	
    if (tiles[south] == 0)
        grassfire[south] = 2;
    else
        grassfire[south] = 255;

    if (tiles[east] == 0)
        grassfire[east] = 2;
    else
        grassfire[east] = 255;

    if (tiles[west] == 0)
        grassfire[west] = 2;
    else
        grassfire[west] = 255;

    if (tiles[center] == 0)
        grassfire[center] = 1;
    if (tiles[center] > 0)
        grassfire[center] = 255;
    //refresh the tiles on moving

Now my tiles constantly update. This is done by always setting the center tile to 1 and the neighbors to 2, therefore when you move in a direction, your old tile which was a 1 would be set to 2 and then the new tile you are on is now a 1.

A horrible way to describe it, how about a few illustrations.

Our original positions, with the kernel colored.

Our player moved up. Notice that the kernel updates, however, now the rest of the Grassfire is now off. That is corrected by the next pass of the Grassfire, which constantly updates. However, for this brief moment in time, the enemy still has a good estimate on how to get to your player.

The second problems I encountered, was that once the enemy had found your tile, it started to wander back and forth, not as much of a problem and more of an annoyance, I wanted the enemy to stop moving once it gets to your tile and not wander back and forth.

This is easily solved with the addition of a few commands in the moveEnemy() found in enemy.h. Find the section where we examine the tile values and place at the bottom.

else if (centerValue == 1)
{
    // STOP, found the target
    enemyDirection = 5;
}

And below that in the section where we actually move the enemy sprite.

if (enemyDirection == 5)//stop
{
    //do nothing
}

and that is taken care of.

The next issue I wanted to take a look at is the problem of clustering, this is the occurrence of enemy sprites riding on top of one another where they are stacked appearing as if there was just 1 sprite instead of multiple sprites. The only way I could come up with was to give the enemy slightly different paths by modifying the Grassfire array where the current enemy sprite was.

This is done in the moveEnemy() found in enemy.h. With the simple command.

grassfire[center] += 8;

This increases the tile that the enemy is on by 8, so when the other enemy sprites encountered this region, they will be encouraged to move around it, again due to the continuous update of the Grassfire algorithm, it should only last a pass or two before the algorithm overwrites the position. It’s not absolute, but it does seem to reduce the number of times that clustering happens.

So, here’s what we have so far.

In movement.h, moveMainCharacter(), we have

void moveMainCharacter(unsigned char movePlayer)
{
    unsigned char spriteFrame;
    unsigned char xCenterofTile, yCenterofTile;
    
    //animation
    {
        if (movePlayer > 8) spriteFrame = 1;
        if (movePlayer < 8) spriteFrame = 0;
    }
    
    //playerX pixel range 0 - 240
    //playerY pixel range 0 - 160    
    //center of tile = (half_tileSize + (tileSize * tileNumber))

    {//move player
        if( Input() & 0x01 ) // P - go right
        {
            sprites[0].n = 0 + spriteFrame;//bubble orientation right 4th sprite over
            ax = vx  -maxvx ? -40 : 0;
        }

        //now we have down doing the same as up for testing
        if( Input() & 0x04 )// A go down
        {
            sprites[0].n = 4 + spriteFrame;//bubble orientation down 4th sprite over
            ay = (vy  -maxvy) ? -40 : -1;
        }
        if( Input() & 0x10 && !spacepressed && num_bullets  0)
        grassfire[center] = 255;
    //refresh the tiles on moving
}

In enemy.h, moveEnemy(), we have

void moveEnemy(unsigned char enemyXX, unsigned char enemyYY, unsigned char enemyFF)
{
    enemySteps = enemyFF << 4;// Strip enemy steps out of enemyFF
    enemySteps = enemySteps >> 4;// adjust variable to the correct range
    enemyDirection = enemyFF << 1;// Strip enemy direction out of enemyFF
    enemyDirection = enemyDirection >> 5;// adjust variable to the correct range
    enemyRetreat = enemyFF >> 7;// Strip enemy retreat out of enemyFF

    //playerX pixel range 0 - 240
    //playerY pixel range 0 - 160
    //enemyX & enemyXX pixel range 0 - 240
    //enemyY & enemyYY pixel range 0 - 160    
    //screen is 15 tiles wide and 10 tiles high
    
    //adjust enemy to character placement from pixel placement
    enemyCharX = enemyXX >> 4;//enemyCharX character range 0 - 15
    enemyCharY = enemyYY >> 4;//enemyCharY character range 0 - 9
    
    //ATTENTION    
    //CenterofTile = (half_tileSize + (tileSize * tileNumber))    
    xCenterofTile = (8+ (16 * enemyCharX));
    yCenterofTile = (8+ (16 * enemyCharY));    
    
    center = enemyCharX + scrw * enemyCharY;
    north = enemyCharX + scrw * (enemyCharY - 1);
    south = enemyCharX + scrw * (enemyCharY + 1);
    east = enemyCharX + 1 + scrw * enemyCharY;
    west = enemyCharX - 1 + scrw * enemyCharY;    
        
    //try to keep the enemies from going into barriers
    if (tiles[north] > 0)
    {
        grassfire[north] = 255;
    }
    if (tiles[south] > 0)
    {
        grassfire[south] = 255;
    }
    if (tiles[east] > 0)
    {
        grassfire[east] = 255;
    }
    if (tiles[west] > 0)
    {
        grassfire[west] = 255;
    }
    //if the enemy is in the center of the tile, need to look which direction we need to travel
    if ((enemyXX == xCenterofTile) && (enemyYY == yCenterofTile))
    {        
        centerValue = grassfire[center];
        northValue = grassfire[north];
        southValue = grassfire[south];
        eastValue = grassfire[east];
        westValue = grassfire[west];
        
        if ((northValue < southValue && northValue < eastValue && northValue < westValue))
        {
            //go north
            enemyDirection = 1;
        }
        else if    ((southValue < northValue && southValue < eastValue && southValue < westValue))
        {
            //go south
            enemyDirection = 2;
        }
        else if    ((eastValue < northValue && eastValue < southValue && eastValue < westValue))
        {
            //go east
            enemyDirection = 3;
        }
        
        else if ((westValue < northValue && westValue < southValue && westValue  232)
        {
            enemyX--;
            enemyDirection = 4;//need to go west
        }
        if (enemyX < 8)
        {
            enemyX++;
            enemyDirection = 3;//need to go east
        }
        if (enemyY  152)
        {
            enemyY--;
            enemyDirection = 1;//need to go north
        }
    }

    {
        if (enemyDirection == 1)//go north/up
        {
            enemyY--;
        }
        if (enemyDirection == 2)//go south/down
        {
            enemyY++;
        }
        if (enemyDirection == 3)//go east/right
        {
            enemyX++;
        }    
        if (enemyDirection == 4)//go west/left
        {
            enemyX--;
        }
        if (enemyDirection == 5)//stop
        {
            //do nothing
        }        
    }
    
    // reduce clustering
    grassfire[center] += 8;
    //to help with clustering
        
    enemyDirection = enemyDirection << 4;
    enemyRetreat = enemyRetreat << 7;
    enemyF = enemySteps | enemyDirection | enemyRetreat;    
}

I have shown both functions for completeness.

Compiling the game and running it, it seems to work rather well, with some issues. The main issue is that occasionally, the enemy still runs into a wall, which is frustrating. For the most part, it works seamlessly, and then a collision occurs that shouldn’t have happened. This is a bit frustrating! It has to be somewhere in the decision-making process.

So it’s not perfect, and now I am faced with a decision to make. However, that decision will have to wait until my next article.

Until then, happy coding.

Advertisements

Author: andydansby

I'm a hobbyist coder working with the ZX Spectrum. Living in New York state near the Syracuse area. I grew up in Virgina. The first computer my parents bought for me was a Timex Sinclair 2068.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s