这可能需要一段时间来解释 - 在你读这篇文章的时候去吃零食。
我正在为C ++中的Gameboy Advance开发一款2D拼图平台游戏(我是一个相当新的程序员)。直到昨晚,我一直在制作一个物理引擎(只是一些轴对齐的边界框的东西),我正在测试使用的水平是GBA屏幕的大小。然而,最终的游戏将需要一个大于屏幕大小的级别,所以我试图实现一个允许GBA的屏幕跟随玩家的系统,因此我必须绘制所有内容在屏幕上相对于屏幕的偏移。
然而,当我显示可以在关卡中拾取和操作的立方体时,我遇到了麻烦。每当玩家移动时,屏幕上立方体的位置似乎偏离其在关卡中的实际位置。这就像绘制立方体的地方是一个不同步的帧 - 当我在玩家移动时暂停游戏时,框会显示在正确的位置,但是当我取消暂停时,它们会偏离地方直到玩家停止再次搬家。
我的类的简短描述 - 有一个名为Object的基类,它定义了(x,y)的位置和宽度和高度,有一个Entity类,它继承自Object并添加了velocity组件,还有一个Character类,继承自Entity并添加移动功能。我的播放器是一个Character对象,而我想要拾取的多维数据集是一个Entity对象数组。 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图块,因此需要每个32x32图块的2x2屏幕块才能显示它们。屏幕块编号为25-28。 103是我的tilemap中的图块编号。
这是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)。类似地,为负Y位置添加256个工作(这都是相对于屏幕而不是水平)。最后一个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));
}
答案 0 :(得分:0)
我将解释这个答案,按位运算符如何工作以及一个数字如何让一个可能值为0到255(256种组合)的字节保存所有GBA控制按键。这类似于你的X / Y位置问题。
控件
Up - Down - Left - Right - A - B - Select - Start
这些是GameBoy Color控件我觉得GameBoy Advanced有更多的控件。
所以共有8个控件。
每个控件都可以按下(按下)或不按下。
这意味着每个控件只应使用数字1
或0
。
由于1
或0
只获取1位信息。在一个字节中,您可以存储多达8个不同的位,这适合所有控件。
现在您可能正在考虑如何通过添加或其他内容将它们组合在一起?是的,你可以做到这一点,但它使理解非常复杂,它给你这个问题。
假设你有一杯半空的水,你又加入了更多的水,你想把新加入的水与旧水分开......你不能这样做,因为水都变成了一种水,无法解除这个(除非我们标记每个水分子,我们还有外星人......哈哈)。
但是对于Bitwise操作,它使用数学来确定整个流(列表)中的哪个位确切地是1
或0
。
所以你要做的第一件事就是把每个位都给控件。 每个位的二进制数为2的倍数,因此您只需将该值加倍。
Up - Down - Left - Right - A - B - Select - Start
1 - 2 - 4 - 8 - 16 - 32 - 64 - 128
此外,按位运算不仅用于确定哪个位是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
所以我要说你一次按下Up
和A
所以你按了1
和16
你创建一个包含所有控件的字节,比如说
unsigned char ControlsPressed = 0;
所以现在没有任何东西被按下,因为它是0。
ControlsPressed |= 1; //Pressed Up
ControlsPressed |= 16; //Pressed A
所以是的,ControlsPressed
现在将持有17
您可能只想1+16
的数字17
这正是它所做的大声笑但是是的水你可以做的事情&#t; t让它回到它的基本价值观,首先使用基本的数学计算。
但是,你可以将16
更改为A
,然后按下向上箭头并按住1+4+16+128
按钮。
但是当你按下很多按钮时,价值变得如此之大。
149
= 149
所以你不记得你添加了什么,但你知道价值是149
你现在如何取回钥匙?好吧,它很简单,是的,只是开始减去你可以找到的控件使用的最高数字,低于ControlsPressed = ControlsPressed AND NOT (NEGATE) Number
,如果你减去它,那么当你减去它然后它不是压下来。
是的,在这一点上,你想是的,我可以制作一些循环并做这些事情,但不需要做任何事情都有内置命令,可以动态执行此操作。
这是您取消按下任何按钮的方法。
ControlsPressed &= ~1; //Let go of Up key.
ControlsPressed &= ~16; //Let go of A key.
在C / C ++ / Javascript中,您可以使用类似这样的内容
<<
还有什么可说的,关于你需要知道的关于按位的东西。
编辑:
我没有解释按位移位运算符>>
或int SomeInteger = 123;
print SomeInteger >> 3;
我真的不知道如何在基本级别上解释这一点。
但是当你看到这样的东西时
SomeInteger = 123 / 8;
有一个右移操作员在那里使用,它向右移3位。
它实际上做的是将2除以3的幂。 所以在基础数学中它真的这样做
>>
所以现在你知道向右移<<
与将值除以2的幂是一回事。
现在转移到左X: (0 to 63)
Y: (0 to 63)
逻辑上意味着你将该值乘以2的幂。
位移主要用于将2种不同的数据类型组合在一起并在以后提取它们。 (我认为这是位移的最常见用途)。
假设您的游戏中有X / Y坐标,每个坐标只能达到有限的值。 (这只是一个例子)
X
而且你也知道X,Y必须存储在一些小的数据类型中。我认为非常紧凑(无间隙)。
(这可能需要一些逆向工程来确定或只是阅读手册)。 可能存在用于保留位或某些未知信息的间隙。
但是在这里移动,所以两者都可以容纳64种不同的组合。
因此,Y
和 X | Y
均为6位,两者共12位。
因此,每个字节总共保存2位。 (共保存4位)。
int X = 33; int Y = 11;
[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 packedValue1 = X << 6; //2112
int packedValue2 = Y << 6; //704
int finalPackedValue = packedValue1 + packedValue2; //2816
由于每个坐标需要6位,这意味着你必须为每个数字向左移动6位。
2816
所以是的,最终价值将是2816
现在,您可以从2816 >> 6 //Gives you back 44. lol.
获取相反方向的相同位置的值。
6
所以,水问题再次发生,你有44(33 + 11)并且没有办法让它回来,这次你不能依靠2的力量来帮助你。
我使用了非常混乱的代码来向您展示为什么必须将其复杂化以防止将来出现错误。
无论如何,回到每个坐标6位以上你必须做的就是拿6并将其添加到那里。
所以现在你有6+6=12
和int packedValue1 = X << 6; //2112
int packedValue2 = Y << 12; //45056
int finalPackedValue = packedValue1 + packedValue2; //47168
。
47168 >> 12; //11
所以是的,最后的价值现在更大了47168 ..但至少现在你将毫无问题地取回价值观。唯一要记住的是你必须先朝相反的方向做最大的转变。
11 << 12; //45056
现在你必须弄清楚11的大号是什么,所以你把它向左移12次。
//47168 - 45056 = 2112
从原始金额中减去
2112 >> 6; //33
现在你可以按6完成右移。
int finalPackedValue = (X << 6) | (Y << 12);
你现在得到了两个值..
使用上面的按位命令可以更轻松地完成打包部件,以便将控件添加到一起。
{{1}}