https://wii.leseratte10.de/devkitPro/devkitARM/我推荐直接用后者,省去一步步安装、污染环境的麻烦。
使用Docker下载devkitARM镜像的命令是(但其实不用手动下载,后面会提到):
docker pull devkitpro/devkitarm
devkitPro提供了大量的3DS示例程序,我们可以随意挑选一个试试我们的工具链是否安装成功了。比如,我想试试能不能编译read-controls这个示例,首先进入它所在的文件夹:
git clone https://github.com/devkitPro/3ds-examples.git
cd 3ds-examples/input/read-controls
此时当前路径下应该有一个Makefile,和带着main.c的source子目录:
tree
# 下面是输出
.
├── Makefile
└── source
└── main.c
1 directory, 2 files
然后使用来自Docker镜像的make来构建程序(如果本地没有devkitARM镜像,这一步会自动拉取):
docker run --rm -v $PWD:/${PWD##*/} devkitpro/devkitarm make -C /${PWD##*/}
如果没有意外,会得到可以被hbmenu运行的read-controls.3dsx。
我们的目标也是要得到3dsx文件,而构建它的规则和示例程序的规则(写在Makefile里)无异。鉴于示例程序的代码采用了放弃一切权利的公共领域授权,我们可以直接修改它的Makefile,据为己有。
简单的游戏可以被分成如下几步:
所以我们的初始代码也如下所示:
#include <3ds.h>
int main(int argc, char* argv[])
{
setup(); // 1
while (aptMainLoop()) { // 2
input(); // 2.1
update(); // 2.2
draw(); // 2.3
}
cleanup(); // 3
return 0;
}
其中setup、draw、cleanup所必须的样板代码,可以在上一小节的示例程序里找到。我就不再赘述了。
另外要注意的是更新频率,如果每个循环蛇都前进一格的话,那就太快了。有些游戏引擎的更新函数会提供“距离上次调用过去了多少时间”的参数,让开发者计算本次更新要更新到什么程度。不过,这种方法对我们的简单游戏有些过火:我们只要给主循环加个不到规定时间不更新的判断即可:
u64 reference = svcGetSystemTick();
bool enoughWait = now - reference > CPU_TICKS_PER_MSEC * 100;
if (enoughWait) {
reference = now;
update();
}
很抱歉,我完全不了解shader、gfx。所以我只能用libctru的console模式打印彩色字符来充当游戏画面。引用一下示例程序的注释:
To move the cursor you have to print "\x1b[r;cH", where r and c are respectively the row and column where you want your cursor to move. The top screen has 30 rows and 50 columns. The bottom screen has 30 rows and 40 columns.
所以在屏幕四周打印一圈井号充当地图边界的代码就是:
using point = std::pair<unsigned, unsigned>;
PrintConsole topScreen;
void putStringAt(const char c[], point p)
{
// 把光标移到(y + 1, x + 1),打印c
// +1是因为libctru的行、列是从一开始数的
printf("\x1b[%d;%dH%s", p.second + 1, p.first + 1, c);
}
void drawMap()
{
consoleSelect(&topScreen);
consoleClear();
// 把光标移到左上角,打印50个#
printf("\x1b[1;1H##################################################");
for (auto i = 1u; i < TOP_SCREEN_HEIGHT - 1; i++) {
// 从第二行到倒数第二行,在最左边和最右边各打印一个#
putStringAt("#", { 0, i });
putStringAt("#", { TOP_SCREEN_WIDTH - 1, i });
}
// 把光标移到左下角,打印50个#
printf("\x1b[30;1H##################################################");
}
在console模式中,背景是黑色的,默认字符颜色是白色。想打印红色的苹果,同样需要\x开头的字符串:\x1b[31m@\x1b[0m。其意为:切换颜色为红色,打印@,恢复原来颜色。所以画苹果的代码是:
point apple;
// 生成苹果位置
putStringAt("\x1b[31m@\x1b[0m", apple);
画蛇和画苹果类似,只是从打印一个@变成打印一个O表示头,再接着几个o表示身子。因为蛇不会只剩一个头,所以遍历身子时可以从1开始。
std::vector<point> snake;
putStringAt("\x1b[32mO\x1b[0m", snake[0]); // 头,绿色大写O
for (auto i = 1uz; i < snake.size(); i++)
putStringAt("\x1b[32mo\x1b[0m", snake[i]); // 身,绿色小写o
但是在更新蛇时,我们不必清空屏幕再重新画。 直接在蛇头前一个画个新头,把原蛇头画成身子,再把蛇尾擦去(打印空格)即可。
putStringAt("\x1b[32mO\x1b[0m", newHead);
putStringAt("\x1b[32mo\x1b[0m", snake.front());
putStringAt(" ", snake.back());
不过这种画法不完全正确,因为蛇吃到苹果时身子会延长,所以在擦除尾巴之前应该先判定蛇头是否碰到苹果了:
if (newHead != apple)
putStringAt(" ", snake.back());
像这种字符画游戏的碰撞检测就是用或连起来一串相等:
bool hitWall(point p)
{
return p.first == 0 || p.first == TOP_SCREEN_WIDTH - 1 || p.second == 0 || p.second == TOP_SCREEN_HEIGHT - 1;
}
生成苹果的方式就是随机,然后看有没有撞到蛇或者墙:
bool hitSnake(point p)
{
return snake.end() != std::find(snake.begin(), snake.end(), p);
}
void createApple()
{
do {
apple = { rand() % TOP_SCREEN_WIDTH, rand() % TOP_SCREEN_HEIGHT };
} while (hitWall(apple) || hitSnake(apple));
}
在libctru里,要先调用hidScanInput,再用hidKeysDown得到按下的按键(或hidKeysHeld得到按住的)。后两者会返回u32类型的编码,和KEY_UP、KEY_DOWN等常量按位与后则可以判定按下的到底是哪些键:
u32 getInput()
{
hidScanInput();
return hidKeysDown() | hidKeysHeld();
}
point getNewHead(u32 key)
{
if (key & KEY_UP)
direction = up;
else if (key & KEY_DOWN)
direction = down;
else if (key & KEY_LEFT)
direction = left;
else if (key & KEY_RIGHT)
direction = right;
point head = snake.front();
switch (direction) {
case up:
return { head.first, head.second - 1 };
case down:
return { head.first, head.second + 1 };
case left:
return { head.first - 1, head.second };
case right:
return { head.first + 1, head.second };
default: // direction is a global variable
__builtin_unreachable();
}
}
把前面各小节的内容组合起来,就是完整的游戏了。184行,不多也不少。
复制以下链接,并粘贴到你的Mastodon、Misskey或GoToSocial等应用的搜索栏中,即可搜到对应本文的嘟文。对嘟文进行的点赞、转发、评论,都会出现在本文底部。快去试试吧!
链接:https://emptystack.top/note/3ds-snake
注:点击昵称可以查看对评论的回复。
@actor 在3DS上用终端写贪吃蛇
很有意思,辛苦了www
@actor@emptystack.top
非常cool的工作,原来3DS是ARM11架构啊
就是用字符画场景有点残念