2

这可能需要一些时间来解释——在你阅读这篇文章的时候去吃点零食。

我正在为 C++ 中的 Gameboy Advance 开发一个 2D 益智平台游戏(我是一个相当新的程序员)。直到昨晚,我一直在制作一个物理引擎(只是一些轴对齐的边界框的东西),我正在使用 GBA 屏幕大小的级别进行测试。但是最终的游戏需要一个大于屏幕大小的关卡,所以我尝试实现一个系统,让GBA的屏幕跟随玩家,结果我必须绘制所有东西在屏幕上相对于屏幕的偏移量。

但是,当我显示可以在关卡中拾取和操作的立方体时,我遇到了麻烦。每当玩家移动时,屏幕上立方体的位置似乎会偏离它们在关卡中的实际位置。就像绘制立方体的位置是一个不同步的帧 - 当我在玩家移动时暂停游戏时,框会显示在完全正确的位置,但是当我取消暂停时,它们会漂移到玩家停止的位置再次移动。

我的类的简要描述 - 有一个名为 Object 的基类,它定义了 (x, y) 位置和宽度和高度,有一个继承自 Object 并添加速度分量的 Entity 类,以及一个继承自 Entity 的 Character 类并增加运动功能。我的播放器是一个角色对象,而我要拾取的立方体是一组实体对象。player 和 cubes 数组都是 Level 类的成员,它也继承自 Object。

我怀疑问题出在最后一个代码示例中,但是,为了完全理解我正在尝试做的事情,我以稍微更合乎逻辑的顺序排列了示例。

以下是 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();
};

...和实体:

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 &);
};

以下是我的主要游戏循环的相关部分:

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

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

    ...
}

由于暂停时精灵显示在正确位置的方式,我很确定问题不在于MoveObjects(),它决定了玩家和立方体在关卡中相对于关卡的位置。所以留下Draw()UpdateLevelObjects()

好的,Draw()。如果不是我的立方体显示不正确,而是它们所在的关卡和平台,我会提供这个(我认为这不是问题,但可能)。Draw()只调用一个相关函数,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);
                    }
                }
            }
        }
    }
}

这不可避免地需要一些解释。我的平台以像素为单位,但显示为 8x8 像素的图块,因此我必须为这个循环划分它们的大小。SetTile()首先需要一个屏幕块编号。我用来显示平台的背景层是 64x64 瓦片,因此需要 2x2 个屏幕块,每个屏幕块 32x32 瓦片才能全部显示。屏幕块编号为 25-28。103 是我的瓷砖地图中的瓷砖编号。

这是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);
    }
}

这些REG_BG位是 GBA 的寄存器,允许背景层垂直和水平偏移多个像素。这些偏移量首先计算在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());
    }
}

需要明确的是,width是指以像素为单位的关卡宽度,而SCREEN_WIDTH指的是GBA屏幕宽度的恒定值。另外,对于懒惰的重复感到抱歉。

这是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);
    }
}

基本上,当玩家位于关卡中间时,它会在屏幕上居中,并在屏幕碰到关卡边缘时允许它移动到边缘。至于立方体:

/**
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);
}

请耐心等待 - 我稍后会解释 512 和 256。这是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(512 和 256)是 GBA 特有的东西 - 将对象的 x 或 y 位置设置为负数不会正常执行您想要的操作 - 它会弄乱用于显示它的精灵。但是有一个技巧:如果你想设置一个负的 X 位置,你可以在负数上加上 512,精灵就会出现在正确的位置(例如,如果你打算将它设置为 -1,那么将它设置为512 + -1 = 511)。同样,添加 256 适用于负 Y 位置(这都是相对于屏幕,而不是级别)。最后一个 if 语句将立方体显示在屏幕之外,如果它们通常显示得更远的话,因为试图将它们显示得太远会导致出现故障方块,这也是 GBA 特定的东西。

如果您已经阅读了所有内容,那么您就是绝对的圣人。如果您能找到可能导致漂移的立方体的原因,我将非常感激。此外,任何一般改进我的代码的提示都将不胜感激。


编辑:GBA对象更新设置播放器和立方体位置的方式如下:

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));
}
4

1 回答 1

1

我将解释这个答案按位运算符是如何工作的,以及一个数字如何让一个可能值为 0 到 255(256 种组合)的字节包含所有 GBA 控制压力机。这类似于您的 X/Y 位置问题。

控件

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

这些是 GameBoy Color 控件,我认为 GameBoy Advanced 有更多控件。所以一共有8个控件。每个控件都可以按下(按住)或不按下。这意味着每个控件应该只使用一个数字10. 因为1or0只需要 1 位信息。在一个字节中,您最多可以存储 8 个不同的位,适合所有控件。

现在您可能在想如何通过添加或其他方式将它们组合在一起?是的,您可以这样做,但是它使理解变得非常复杂,并且给您带来了这个问题。

假设你有一杯半空的水,你往里面加了更多的水,你想把新加的水和旧水分开。你不能这样做,因为水都变成了一种水,没有办法撤消此操作(除非我们标记每个水分子并且我们还不是外星人......哈哈)。

但是对于按位运算,它使用数学来确定哪个位确切地是一个10在整个位流(列表)中。

所以你要做的第一件事就是把每一位都交给一个控件。每个位都是二进制的 2 的倍数,因此您只需将值加倍即可。

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

此外,按位运算不仅用于确定哪个位是 a 1,或者0您还可以使用它们将某些事物组合在一起。控件做得很好,因为您可以一次按住多个按钮。

这是我用来确定按下或未按下的代码。

我不使用 C/C++,所以这是javascript我在我的 gameboy 模拟器网站上使用的,字符串部分可能是错误的,但实际的按位代码在几乎所有编程语言中都是通用的,我看到的唯一区别是 Visual Basic&会在AND那里调用.

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;
}

您如何设置要按下的单个控件?好吧,你必须记住你用哪个位数字来控制我会做这样的事情

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

所以让我们说你按下UpA立即所以你按下116

您制作了 1 个字节来保存所有控件,可以说

unsigned char ControlsPressed = 0;

所以现在什么都没有按下,因为它是 0。

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

所以,是的,ControlsPressed现在将持有17你可能在想的数字,1+16这正是它所做的事情,哈哈,但是是的,水的东西你不能让它回到它的基本值,它首先使用基本数学来弥补。

但是,是的,您可以将其更改17为,16然后松开向上箭头并按住A按钮。

但是,当您按住很多按钮时,可以说价值变得如此之大。 1+4+16+128=149

所以你不记得你加了什么,但你知道价值是149你现在如何取回密钥?好吧,这很容易,是的,只需开始减去您可以找到的控件使用的最高数字,然后再149减去,如果减去它时它更大,那么它就不会被按下。

是的,此时您认为是的,我可以制作一些循环并执行此操作,但这一切都不需要完成,内置命令可以即时执行此操作。

这就是您松开任何按钮的方式。

ControlsPressed = ControlsPressed AND NOT (NEGATE) Number

在 C/C++/Javascript 中,你可以使用类似这样的东西

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

还有什么要说的是关于你需要知道的关于按位的东西的一切。

编辑:

我没有解释按位移位运算符<<,或者>>我真的不知道如何在基本层面上解释这一点。

但是当你看到这样的东西

int SomeInteger = 123;
print SomeInteger >> 3;

那里有一个右移运算符在那里使用,它向右移动 3 位。

它实际上做的是除以 2 的 3 次方。所以在基础数学中它确实是这样做的

SomeInteger = 123 / 8;

所以现在您知道向右移动与>>将值除以 2 的幂是一样的。现在向左移动在<<逻辑上意味着您将值乘以 2 的幂。

位移位主要用于将 2 种不同的数据类型打包在一起并在以后提取它们。(我认为这是移位最常见的用法)。

假设您的游戏中有 X/Y 坐标,每个坐标只能达到有限值。(这只是一个例子)

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

而且您还知道 X,Y 必须存储到一些小的数据类型中。我假设包装非常紧密(没有间隙)。

(这可能需要一些逆向工程才能准确找出或只是阅读手册)。那里可能有用于保留位或某些未知信息的空白。

但是在这里继续前进,因此两者都可以容纳总共 64 种不同的组合。

所以两者XY占用 6 位,总共 12 位。因此,每个字节总共保存 2 位。(总共节省 4 位)。

 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]

所以你需要使用位移来正确存储信息。

这是您存储它们的方式

int X = 33;
int Y = 11;

由于每个坐标占用 6 位,这意味着您必须为每个数字左移 6。

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

所以是的,最终价值将是2816

2816现在,您通过在相反方向上进行相同的移动来获取值。

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

所以是的,水的问题又发生了,你有 44 (33+11) 并且没有办法让它回来,这一次你不能依靠 2 的力量来帮助你。

我使用了非常混乱的代码来向您展示为什么您必须故意将其复杂化以防止将来出现错误。

无论如何,回到每个坐标的 6 位以上,您必须做的是取 6 并将其添加到那里。

所以现在你有6and 6+6=12

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

所以是的,最终值现在更大了 47168 .. 但至少现在你将完全没有问题取回这些值。唯一要记住的是,您必须首先朝相反的方向进行最大的转变。

47168 >> 12; //11

现在你必须弄清楚大数字 11 是由什么组成的,所以你将它向左移动 12 次。

11 << 12; //45056

从原始总和中减去

//47168 - 45056 = 2112

现在你可以在 6 点之前完成右移。

2112 >> 6; //33

你现在得到了两个值..

您可以使用上面的按位命令更轻松地完成打包部分,以便将控件添加在一起。

int finalPackedValue = (X << 6) | (Y << 12);
于 2014-05-18T06:18:35.840 回答