GameBoy Advance objects not being displayed in correct place when player is moving

老子叫甜甜 提交于 2019-12-11 08:29:52

问题


This might take a while to explain - go grab a snack while you're reading this.

I am developing a 2D puzzle platforming game for the Gameboy Advance in C++ (I'm a fairly new programmer). Up until last night, I have been making a phyics engine (just some axis aligned bounding box stuff) which I was testing using a level which was the size of the GBA's screen. However, the final game will demand having a level which is bigger than the size of the screen, and so I have tried to implement a system which allows the screen of the GBA to follow the player, and as a result I have to draw everything on screen relative to the screen's offsets.

However, I am having trouble when I display cubes which can be picked up and manipulated in the level. Whenever the player moves, the locations of the cubes on screen appear to drift away from their actual positions in the level. It's like where the cubes are drawn is a single frame out of sync - when I pause the game when the player is moving, the boxes are displayed in exactly the right position, but when I unpause, they drift out of place until the player stops moving again.

A brief description of my classes - there is a base class called Object which defines (x, y) position and a width and height, there is an Entity class which inherits from Object and adds velocity components, and a Character class which inherits from Entity and adds movement functions. My player is a Character object, while the cubes I want to pick up are an array of Entity objects. Both the player and cubes array are members of the Level class, which also inherits from Object.

I suspect the problem lies in the last code sample, however, for full comprehension of what I am trying to do I have laid out the samples in a slightly more logical order.

Here are the truncated headers of Level:

class Level : public Object
{
    private:
        //Data
        int backgroundoffsetx;
        int backgroundoffsety;

        //Methods
        void ApplyEntityOffsets();
        void DetermineBackgroundOffsets();

    public:
        //Data
        enum {MAXCUBES = 20};

        Entity cube[MAXCUBES];
        Character player;
        int numofcubes;

        //Methods
        Level();
        void Draw();
        void DrawBackground(dimension);
        void UpdateLevelObjects();
};

...and Entity:

class Entity : public Object
{   
    private:
        //Methods
        int GetScreenAxis(int &, int &, const int, int &, const int);

    public: 
        //Data
        int drawx;  //Where the Entity's x position is relative to the screen
        int drawy;  //Where the Entity's y position is relative to the screen

        //Methods
        void SetScreenPosition(int &, int &);
};

Here are the relevant parts of my main game loop:

//Main loop
while (true)
{
    ...

    level.MoveObjects(buttons);
    level.Draw();
    level.UpdateLevelObjects();

    ...
}

Because of the way sprites are displayed in the correct places when paused, I'm pretty sure the problem does not lie in MoveObjects(), which determines the poitions of the player and cubes in the level relative to the level. So that leaves Draw() and UpdateLevelObjects().

Ok, Draw(). I'm providing this in the event that it is not my cubes that are being displayed incorrectly, but the level and platforms upon which they sit (I don't think this is the problem, but possibly). Draw() only calls one relevant function, DrawBackground():

/**
Draws the background of the level;
*/
void Level::DrawBackground(dimension curdimension)
{
    ...

    //Platforms
    for (int i = 0; i < numofplatforms; i++)
    {
        for (int y = platform[i].Gety() / 8 ; y < platform[i].GetBottom() / 8; y++)
        {
            for (int x = platform[i].Getx() / 8; x < platform[i].GetRight() / 8; x++)
            {
                if (x < 32)
                {
                    if (y < 32)
                    {
                        SetTile(25, x, y, 103);
                    }
                    else
                    {
                        SetTile(27, x, y - 32, 103);
                    }
                }
                else
                {
                    if (y < 32)
                    {
                        SetTile(26, x - 32, y, 103);
                    }
                    else
                    {
                        SetTile(28, x - 32, y - 32, 103);
                    }
                }
            }
        }
    }
}

This inevitably requires some amount of explaining. My platforms are measured in pixels, but displayed in tiles of 8x8 pixels, so I have to divide their sizes for this loop. SetTile() firstly requires a screenblock number. The background layer I am using to display the platforms is 64x64 tiles, and so requires 2x2 screenblocks of 32x32 tiles each to display them all. The screenblocks are numbered 25-28. 103 is the tile number in my tilemap.

Here's UpdateLevelObjects():

/**
Updates all gba objects in Level
*/
void Level::UpdateLevelObjects()
{
    DetermineBackgroundOffsets();
    ApplyEntityOffsets();

    REG_BG2HOFS = backgroundoffsetx;
    REG_BG3HOFS = backgroundoffsetx / 2;    
    REG_BG2VOFS = backgroundoffsety;
    REG_BG3VOFS = backgroundoffsety / 2;

    ...

    //Code which sets player position (drawx, drawy);

    //Draw cubes
    for (int i = 0; i < numofcubes; i++)
    {
        //Code which sets cube[i] position to (drawx, drawy);
    }
}

The REG_BG bits are the registers of the GBA which allow the background layers to be offset vertically and horizontally by a number of pixels. Those offsets are first calculated in DetermineBackgroundOffsets():

/**
Calculate the offsets of screen based on where the player is in the level
*/
void Level::DetermineBackgroundOffsets()
{
    if (player.Getx() < SCREEN_WIDTH / 2)   //If player is less than half the width of the screen away from the left wall of the level
    {
        backgroundoffsetx = 0;
    }
    else if (player.Getx() > width - (SCREEN_WIDTH / 2))    //If player is less than half the width of the screen away from the right wall of the level
    {
        backgroundoffsetx = width - SCREEN_WIDTH;   
    }
    else    //If the player is in the middle of the level
    {
        backgroundoffsetx = -((SCREEN_WIDTH / 2) - player.Getx());
    }

    if (player.Gety() < SCREEN_HEIGHT / 2)
    {
        backgroundoffsety = 0;
    }
    else if (player.Gety() > height - (SCREEN_HEIGHT / 2))
    {
        backgroundoffsety = height - SCREEN_HEIGHT; 
    }
    else
    {
        backgroundoffsety = -((SCREEN_HEIGHT / 2) - player.Gety());
    }
}

Just to be clear, width refers to the width of the level in pixels, while SCREEN_WIDTH refers to the constant value of the width of the GBA's screen. Also, sorry for the lazy repetition.

Here's ApplyEntityOffsets:

/**
Determines the offsets that keep the player in the middle of the screen
*/
void Level::ApplyEntityOffsets()
{
    //Player offsets
    player.drawx = player.Getx() - backgroundoffsetx;
    player.drawy = player.Gety() - backgroundoffsety;

    //Cube offsets
    for (int i = 0; i < numofcubes; i++)
    {
        cube[i].SetScreenPosition(backgroundoffsetx, backgroundoffsety);
    }
}

Basically this centres the player on the screen when it is in the middle of the level, and allows it to move to edges when the screen bumps against the edge of the level. As for the cubes:

/**
Determines the x and y positions of an entity relative to the screen
*/
void Entity::SetScreenPosition(int &backgroundoffsetx, int &backgroundoffsety)
{
    drawx = GetScreenAxis(x, width, 512, backgroundoffsetx, SCREEN_WIDTH);
    drawy = GetScreenAxis(y, height, 256, backgroundoffsety, SCREEN_HEIGHT);
}

Bear with me - I will explain the 512 and 256 in a moment. Here's GetScreenAxis():

/**
Sets the position along an axis of an entity relative to the screen's position
*/
int Entity::GetScreenAxis(int &axis, int &dimensioninaxis, const int OBJECT_OFFSET, 
                            int &backgroundoffsetaxis, const int SCREEN_DIMENSION)
{
    int newposition;
    bool onawkwardedgeofscreen = false;

    //If position of entity is partially off screen in -ve direction
    if (axis - backgroundoffsetaxis < dimensioninaxis)
    {
        newposition = axis - backgroundoffsetaxis + OBJECT_OFFSET;
        onawkwardedgeofscreen = true;
    }
    else
    {
        newposition = axis - backgroundoffsetaxis;
    }

    if ((newposition > SCREEN_DIMENSION) && !onawkwardedgeofscreen)
    {
        newposition = SCREEN_DIMENSION;     //Gets rid of glitchy squares appearing on screen
    }

    return newposition;
}

OBJECT_OFFSET (the 512 and 256) is a GBA specific thing - setting an object's x or y position to a negative number won't do what you intend normally - it messes up the sprite used to display it. But there's a trick: if you want to set a negative X position, you can add 512 to the negative number, and the sprite will appear in the right place (e.g. if you were going to set it to -1, then set it to 512 + -1 = 511). Similarly, adding 256 works for negative Y positions (this is all relative to the screen, not the level). The last if statement keeps the cubes displayed fractionally off the screen if they would normally be displayed further away, as trying to display them too far away results in glitchy squares appearing, again GBA specific stuff.

You are an absolute saint if you have come this far having read everything. If you can find what potentially might be causing the drifting cubes, I will be VERY grateful. Also, any tips to generally improve my code will be appreciated.


Edit: The way the GBA's objects are updated for setting player and the cubes' positions is as follows:

for (int i = 0; i < numofcubes; i++)
{
    SetObject(cube[i].GetObjNum(),
      ATTR0_SHAPE(0) | ATTR0_8BPP | ATTR0_REG | ATTR0_Y(cube[i].drawy),
      ATTR1_SIZE(0) | ATTR1_X(cube[i].drawx),
      ATTR2_ID8(0) | ATTR2_PRIO(2));
}

回答1:


I will explain this answer how bitwise operators work and how one number lets say a byte with a possible value of 0 to 255 (256 combinations) holds all the GBA Control presses. Which is similar to your X/Y position problem.

The controls

Up - Down - Left - Right - A - B - Select - Start

Those are the GameBoy Color controls I think GameBoy Advanced has more controls. So a total of 8 controls. Each control can either be pressed (held down) or not pressed. That would mean the each control should only be using a number 1 or 0. Since 1 or 0 takes only 1 bit of information. In one byte you can store up to 8 different bits, which fits all the controls.

Now you may be thinking how can I combine them together by adding or something? yes you can do that but it makes it very complicated to understand and it gives you this problem.

Say you have a glass of water that's half empty and you add more water into it and you want to separate the newly added water from the old water.. you just can't do that because the water all became one water with no way to undo this (unless we label each water moleclue and we ain't aliens yet.. lol).

But with Bitwise operations it uses math to figure out which bit exactly is a 1 or 0 in the whole stream (list) of bits.

So first thing you do is you give each bit to a control. Each bit is in binary a multiple of 2, so you just keep doubling the value.

Up - Down - Left - Right - A - B - Select - Start
1 - 2 - 4 - 8 - 16 - 32 - 64 - 128

Also bitwise operations are not only used to figure out which bit is a 1 or 0 you could also use them to combine certain things together. Controls do this well since you can press and hold multiple buttons at once.

Here is the code I use to figure out which is pressed or not pressed.

I don't use C/C++ so this is javascript I used this for my gameboy emulator website the string part may be wrong but the actual bitwise code is universal on nearly all programing languages, only difference I seen is Visual Basic the & would be called AND there.

function WhatControlsPressed(controlsByte) {
    var controlsPressed = " ";
    if (controlsByte & 1) {
        controlsPressed = controlsPressed + "up "
    }
    if (controlsByte & 2) {
        controlsPressed = controlsPressed + "down "
    }
    if (controlsByte & 4) {
        controlsPressed = controlsPressed + "left "
    }
    if (controlsByte & 8) {
        controlsPressed = controlsPressed + "right "
    }
    if (controlsByte & 16) {
        controlsPressed = controlsPressed + "a "
    }
    if (controlsByte & 32) {
        controlsPressed = controlsPressed + "b "
    }
    if (controlsByte & 64) {
        controlsPressed = controlsPressed + "select "
    }
    if (controlsByte & 128) {
        controlsPressed = controlsPressed + "start "
    }
    return controlsPressed;
}

How do you set a individual control to be pressed? well you have to remember which bitwise number you used for what control I would make something like this

#DEFINE UP 1
#DEFINE DOWN 2
#DFFINE LEFT 4
#DEFINE RIGHT 8

So lets say you press down Up and A at once So you pressed 1 and 16

You make 1 byte that holds all the controls lets say

unsigned char ControlsPressed = 0;

So nothing is pressed now because it's 0.

ControlsPressed |= 1; //Pressed Up
ControlsPressed |= 16; //Pressed A

So yeah the ControlsPressed will now be holding the number 17 you may be thinking just 1+16 which is exactly what it does lol but yeah the water thing you can't get it back to it's basic values that made it up in the first place using basic math.

But yeah you could change that 17 to 16 and bam you let go off the Up arrow and just holding down the A button.

But when you holding down lots of buttons the value gets so big lets say. 1+4+16+128 = 149

So you don't remember that what you added up but you know the value is 149 how will you get back the keys now? well it's pretty easy yeah just start subtracting the highest number you can find your controls use that is lower then 149 and you if it's bigger when you subtract it then it's not pressed down.

Yeah at this point you thinking yeah I could make some loops and do this stuff but it's all no needed to be done there is built-in commands that do this on the fly.

This is how you unpress any of the buttons.

ControlsPressed = ControlsPressed AND NOT (NEGATE) Number

In C/C++/Javascript you can use something like this

ControlsPressed &= ~1; //Let go of Up key.
ControlsPressed &= ~16; //Let go of A key.

What else to say that's about everything you need to know about the bitwise stuff.

EDIT:

I didn't explain the bitwise shifting operators << or >> I really don't know how to explain this on a basic level.

But when you see something like this

int SomeInteger = 123;
print SomeInteger >> 3;

That up there is a shift right operator getting used there and it's shifting 3 bits to the right.

What it actually does is divide by 2 to the power of 3. So in basic math it's really doing this

SomeInteger = 123 / 8;

So now you know that shifting to the right >> is the same thing as dividing the value by powers of 2. Now shifting to the left << would logically mean you are multiplying the value by the powers of 2.

Bit shifting is mostly used to pack 2 different datatypes together and extract them later. (I think this is the most common use of the bit shift).

Say you have X/Y Coordinates in your game each coordinate can only go to a limited value. (This is just a example)

X: (0 to 63)
Y: (0 to 63)

And you also know that X,Y must be stored into some small datatype. I assume very tightly packed (no gaps).

(this may take some reverse engineering to figure out exactly or just reading manuals). There could be gaps in there used for reserved bits or some unknown information.

But moving along here so both can hold a total of 64 different combinations.

So both X and Y each take 6 bits, 12 bits in total for both. So a total of 2 bits are saved for each byte. (4 bits saved in total).

 X         |       Y

[1 2 4 8 16 32] |[1 2 4 8 16 32]
[1 2 4 8 16 32 64][128 1 2 4 8 [16 32 64 128]

So you need to use bitshifting to store information properly.

Here is how you store them

int X = 33;
int Y = 11;

Since each coordinate takes 6 bits that would mean you have to shift left by 6 for each number.

int packedValue1 = X << 6; //2112
int packedValue2 = Y << 6; //704
int finalPackedValue = packedValue1 + packedValue2; //2816

So yeah the final value will be 2816

Now you get the values back from 2816 doing the same shift in the opposite direction.

2816 >> 6 //Gives you back 44. lol.

So yeah the problem with the water happened again you have 44 (33+11) and no way to get it back and this time you can't rely on the powers of 2 to help you out.

I used very messy code to show you why you must complicate it on purpose to fend of bugs in the future.

Anyways back to above its 6 bits per coordinate what you must do is take the 6 and add it there.

so now you have 6 and 6+6=12.

int packedValue1 = X << 6; //2112
int packedValue2 = Y << 12; //45056
int finalPackedValue = packedValue1 + packedValue2; //47168

So yeah the final value is now bigger 47168.. But atleast now you will have no problems at all getting back the values. Only thing to remember you must do it in opposite direction biggest shift first.

47168 >> 12; //11

Now you have to figure out what big number 11 is made of so you shift it back left 12 times.

11 << 12; //45056

Subtract from original sum

//47168 - 45056 = 2112

Now you can finish the shift right by 6.

2112 >> 6; //33

You now got both values back..

You can do the packing part much easier with the bitwise command above for adding the controls up together.

int finalPackedValue = (X << 6) | (Y << 12);


来源:https://stackoverflow.com/questions/23718832/gameboy-advance-objects-not-being-displayed-in-correct-place-when-player-is-movi

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!