PSP 开发/Hello World 应用
本文的目标是概述一个基本的、标准的最小工作示例 (MWE),以演示核心通用实践。这些内容应该与每个项目都需要的操作相关。 获取工具 应该已经完成,或者正在等待下载完成。
在 /pspsdk 中是一个开始项目文件夹库的好地方。有些人更喜欢使用 IDE。了解 IDE 的工作原理以及使用正确的 Makefile 样式项目是必要的。构建使用 PSP-GCC 和 pspsdk/bin 中的工具进行。程序的 C 编译器的 bin 是 make 所需的。这些文章将直接手动编辑 Makefile,因为它包含从正常、日常 Makefile 中添加的内容,以正确编译。外部 Makefile 包含在 Makefile 中,并且变量在预先设置。
一个从读者到作者无缝转换的通用工作区非常重要。它使文章的传播变得更加容易。虽然不建议初学者跳过步骤,但有经验的 PSP 程序员或熟练的程序员会发现很容易偏离文章。项目文件夹应该在你的 PSPDEV 文件夹中找到。另一个好的做法是在你的用户文件夹中创建一个 projects 文件夹。
对于整个维基教科书,将使用一个通用的 projects 文件夹。Windows 将有一个 C:/pspsdk/projects 文件夹。Linux 将有一个 /opt/pspsdk/projects 文件夹。除了 linux 之外,Mac 可能还有 /usr/local/pspsdk/projects。通用文件夹包含源代码、头文件和对象文件,这些文件应该与项目共享并根据需要编辑。项目文件夹内部是一个放置通用文件夹的好地方。一些代码是标准的,因此明智的做法不仅是减少编译时间,而且是整理一个整洁的文件夹以提高生产力。这些文件应该稍微编辑,并且不要在项目之间出现错误,因为对项目进行迭代以修复任何错误或应用任何改进将是一个非常频繁的操作。
- 导航到 pspsdk 文件夹
- 创建一个名为 projects 的文件夹
- 导航到 pspsdk/projects 文件夹
- 创建一个名为 common 的文件夹
虽然命名约定略微重要,但指定项目内容的全面名称非常有用。这在评估项目和探索文件系统时寻找特定项目时可以避免混淆。整个路径中不应该包含空格,以减少可能出现的大量问题,这些问题与路径的工作方式有关。路径 opt/psp sdk/projects/project 实际上将读取为两个单独的参数。为了避免这种情况,在路径周围添加嵌套引号。 "opt/psp sdk/projects/project" 将被读取为一个参数。
- 导航到 projects 文件夹
- 创建一个名为 hello_world 的项目文件夹
回调系统存在于每个 PSP 项目中。明智的做法是为此创建一个编译单元,因为它不会在项目之间发生变化。
- 导航到 common 文件夹
- 插入一个新的 C 编译单元,并带有相应的头文件
- 将这些文件命名为 callback.c 和 callback.h
- 确保你在编译单元中包含了头文件。
进入 hello_world 项目的入口点是必要的。这个编译单元叫做 main.c,没有头文件。
- 导航到 hello_world 文件夹
- 插入一个名为 main.c 的新编译单元
main.c 文件是所有过程代码以及必要的特定设置过程所在的位置。main.c 应该看起来像下面的文件,并包含 common/callback.h 文件。
main.c
#include "../common/callback.h"
int main(int argc, char** argv)
{
return 0;
}
PSPDEV 库有许多不同的命名约定。此列表不是一个完全完整的列表。
- 函数
- sceFunctionName()
- pspFunctionName()
- 库
- liblibrary.*
- psplibrary.*
- 结构/数据类型
- SceType
- Sce缩写
使用 common/callback.* 编译单元的主要原因是包含不应更改或很少更改的代码。在这种情况下,PSP 请求程序处理的一种方法。需要注册一个线程,它会调用一个函数作为回调,或者看起来像一个事件,告诉程序在用户(或程序)请求关闭时退出。线程被休眠以使其保持活动状态。相应的文件将包含以下代码。
callback.c
#include <pspkernel.h>
static int exitRequest = 1;
int isRunning()
{
return exitRequest;
}
int exitCallback(int arg1, int arg2, void *common)
{
exitRequest = 0;
return 0;
}
int callbackThread(SceSize args, void *argp)
{
int callbackID;
callbackID = sceKernelCreateCallback("Exit Callback", exitCallback, NULL);
sceKernelRegisterExitCallback(callbackID);
sceKernelSleepThreadCB();
return 0;
}
int setupExitCallback()
{
int threadID = 0;
threadID = sceKernelCreateThread("Callback Update Thread", callbackThread, 0x11, 0xFA0, THREAD_ATTR_USER, 0);
if(threadID >= 0)
{
sceKernelStartThread(threadID, 0, 0);
}
return threadID;
}
callback.h
#ifndef COMMON_CALLBACK_H
#define COMMON_CALLBACK_H
int isRunning();
int setupExitCallback();
#endif
查看头文件,它公开了两个函数:isRunning() 和 setupExitCallback()。isRunning() 函数将确定何时退出主游戏循环,该循环每帧更新一次,以便程序继续正常运行,以及在停止后清理、保存并在退出请求发生后退出程序。当用户请求退出时,exitCallback 函数将被调用,并将 exitRequest 从 0(无退出请求)切换到 1(有效退出请求)。
在 main.c 编译单元中,PSP 要求程序员在文件中设置特定参数。在进行此设置之前,需要包含标题文件以进行操作。通常的做法是创建指向非常长、经常输入的函数名称的宏。将所有使用的内容都用宏定义被认为是不好的做法。
操作所需的包含文件是 pspkernel.h、pspdebug.h 和 pspdisplay.h。有一个可用的调试屏幕,可以在其中输出类似于操作系统中文本模式的文本。内核和显示对于许多编译单元至关重要,因为它们提供了通用的核心功能。这些标题文件可以在 pspsdk/psp/sdk/include 中找到,但它会分支到多个包含文件中。标题文件中大部分代码都带有注释。建议在使用过程中熟悉函数名称。
包含 hello_world 项目所需的标题文件。
main.c
#include <pspkernel.h>
#include <pspdebug.h>
#include <pspdisplay.h>
在标题文件之后,PSP 要求程序员定义环境。使用 PSPDEV 提供的函数,可以填写信息 - 也可以添加额外的信息。PSP_MODULE_INFO 函数设置了程序的基本信息,并且在所有项目中都是必要的。第一个参数是模块的标题。第二个参数是用户模式或内核模式。始终使用用户模式,除非需要内核模式,内核模式在除修改 PSP 之外的其他情况下很少使用。最后两个参数是定义版本和修订号的数字。PSP_MAIN_THREAD_ATTR 函数设置了主线程遵守的环境。当不尝试在内核模式下工作时,此函数是可选的。建议远离内核模式,因为你可能会损坏 你的 PSP。
在包含文件之后添加所需的代码。
main.c
#define VERS 1 // version
#define REVS 0 // revision
PSP_MODULE_INFO("HelloWorld", PSP_MODULE_USER, VERS, REVS);
PSP_MAIN_THREAD_ATTR(PSP_THREAD_ATTR_USER);
在主编译单元中需要进行进一步设置。需要调用退出回调函数,程序需要正式退出,并在其间需要一个游戏循环来更新每一帧。但是,在游戏循环之前是初始设置发生的地方,但是,控制台程序和基于游戏的编程之间的区别在于变量的持久性。将信息放在 main 函数之外或在 main 函数之外创建变量并不罕见。在进行多线程操作时,这是一种常见的做法。
函数 int main(int argc, char** argv) 从 PPSSPP 运行时提供了总共 1 个参数。argc 的值是传递给程序的参数数量的长度,这些参数存储在 argv 中。argv 的第一个索引返回 EBOOT.PBP 可执行文件的当前路径,它是 PSP 的可执行文件(.exe)。使用 PPSSPP 模拟器进行测试,你会得到 'umd0:/EBOOT.PBP'。这在实际的 PSP 上可能并非如此。
需要使用在 ../common/callback.c 中创建的函数。这是 main 函数中的第一个调用。在进行此调用之后,程序员可以自由控制程序的操作。最后,返回退出代码。正常退出应该返回标准的 0。在 main 函数返回之前,需要调用退出函数 sceKernelExitGame()。
注意(在主线程中调用 sceKernelExitGame() 是不好的做法。文档注释建议在单独的线程中调用 sceKernelExitGame() - 例如回调线程。这意味着需要对线程进行一些线程管理,并在清理和文件保存操作之后进行操作。sceKernelExitGame() 只是会使游戏崩溃,这对程序来说影响不大。它在功能上等效于 exit()。但是,它可能需要长达 20 秒的时间才会崩溃。在上面的当前设置中,让回调线程调用 sceKernelExitGame() 的一个问题是无法在 while 循环之后进行编程,因为程序将在那时退出,而不是在程序清理和保存数据之后退出。)
在 main 函数内部添加所需的代码。
main.c
int main(int argc, char** argv)
{
// basic init
setupExitCallback();
pspDebugScreenInit();
// basic exit by crashing
sceKernelExitGame();
return 0;
}
程序的基本模板已完成。构建和测试看起来像是崩溃了,除非实时发生了一些事情,这意味着 PSP 正在积极运行该程序。一个广泛使用的功能是调试屏幕。调试屏幕在功能上类似于操作系统中的文本模式。虽然调试屏幕可以以多种颜色输出文本,但默认颜色是黑色背景上的白色文本。
写入屏幕的函数是 pspDebugScreenPrintf。它在功能上等效于 stdio.h 中的 printf(),因为 printf("%s%d", "hello", 123) 会将 hello123 打印到调试屏幕上。一个常见、可接受且安全的做法是将 pspDebugScreenPrintf 定义为 printf - 因为它很可能多次调用此函数,并且每次都得到相同的结果。
注意:(如果使用 printf("%s", (int) 123),可能会发生看似随机的崩溃。如果使用 printf((int) 123),会发生崩溃。必须始终提供正确的格式。)
在文件顶部的设置函数之后添加以下内容。
main.c
# define printf pspDebugScreenPrintf
需要一个 while 循环(主循环)来挂起主线程,并使每帧的图形和逻辑都能更新。使用调试屏幕,需要在每帧打印一次,这样才能在清除屏幕时保持可见。如果屏幕被清除并且只打印一次,那么它只会显示一次然后消失。这意味着你在主循环之前打印的任何内容都不会保留,除非程序在打印之后挂起。为了解决这个问题,在主循环中适当的时候增强打印命令。调用 printf("\n") 会换行。在 printf() 调用之间,文本可能会被覆盖,因为 printf() 每次在 X 轴上的相同位置开始。有一个内部机制来跟踪符号绘制系统的定位。在绘制任何内容之前,屏幕的左上角会重新打印所有信息。pspDebugScreenSetXY(0, 0) 会修改内部光标的操作位置。在使用调试屏幕之前,需要对其进行初始化。在主循环之前使用 pspDebugScreenInit() 是一个合适的位置。
关于主循环,需要澄清一些事情。首先,它会以帧速率更新每一帧。第二件事是它需要进入一个叫做 vblank 的状态。VBlank 代表垂直空白。关于为什么需要这样做,其描述可以追溯到控制台硬件。有关 vblank 的更多信息,请参阅维基百科上的 VBlank。简而言之,程序在读取像素时写入像素会产生图形错误。其中一种错误是撕裂。
将使用 ../common/callback.c 中的 isRunning() 函数来保持主循环处于活动状态。它会返回一个标志,用于确定游戏是否应该开始退出。在主循环的开始处进入 vblank 是最佳做法,但是如果在结束处进行,那么第一帧(单帧)将出现图形问题。由于渲染速度很快,这不会被人眼或注意力跨度注意到。
任何动态文本都会有问题。例如,绘制一个包含 20 个字符的字符串和一个包含 5 个字符的字符串,它们在同一行上绘制,会发生重叠,看起来像是 20 个字符字符串的前 5 个字符被替换了,而其他 15 个字符从未被删除。绘制的任何文本都会保留在屏幕上。为了解决这个问题,在绘制之前使用 pspDebugScreenClear() 清除每一帧的屏幕。这个调用应该放在 vblank 行之后,在打印发生之前。
main.c
pspDebugScreenInit(); // initialize the debug screen
while(isRunning()) { // while this program is alive
sceDisplayWaitVblankStart(); // wait for vblank
pspDebugScreenClear(); // clears screen pixels
pspDebugScreenSetXY(0, 0); // reset where we draw
printf("Hello World!"); // print some text
}
当涉及到程序员理解正在发生的事情时,让计算机为你编辑 Makefile 是有问题的,特别是如果他们没有使用 Makefile 的经验。遵循本文演示的常见做法非常重要。本文无法详细解释 Makefile 的工作机制,但是由于它是必须做的事情,因此需要用一些实例来解释其完整性。
在 hello_world 项目文件夹的根目录中创建一个名为 Makefile 的新文件。不要添加任何扩展名。添加必要的代码。
Makefile
TARGET = hello_world
OBJS = main.o ../common/callback.o
INCDIR =
CFLAGS = -O2 -G0 -Wall
CXXFLAGS = $(CFLAGS) -fno-exceptions -fno-rtti
ASFLAGS = $(CFLAGS)
LIBDIR =
LIBS =
LDFLAGS =
EXTRA_TARGETS = EBOOT.PBP
PSP_EBOOT_TITLE = HelloWorld
PSPSDK=$(shell psp-config --pspsdk-path)
include $(PSPSDK)/lib/build.mak
你创建的 Makefile 将包含 PSPDEV 提供的另一个 Makefile。程序员创建的 Makefile 只指示后面的 Makefile 做什么。如果向项目中添加了库,则需要在 LIBS 下列出它们。库文件将位于 pspsdk/psp/sdk/lib 文件夹中。如果未存储在 lib 文件夹中,则库应该位于项目文件夹中。如果在库之前提供了 -l,则可以从库中删除 lib 和 .a 扩展名。liblua.a 变为 -llua。使用的任何编译单元都需要在 OBJS 下添加。在本文中,main.o 和 ../common/callback.o 是使用的编译单元,需要将它们添加到 Makefile OBJS 中。PSP_EBOOT_TITLE 应该与程序中调用 PSP_MODULE_INFO 时提供的名称匹配。项目文件夹的名称应该与 TARGET 匹配。
运行 PPSSPP 或 PSP 需要 EBOOT.PBP 文件。EBOOT.PBP 是 PSP 的可执行文件。如果出现任何编译错误,程序员需要在生成文件之前修复它们。EBOOT.PBP 文件是从 ELF 文件的变体生成的。
- 在 cmd 或终端中导航到项目文件夹
- 执行 "make EBOOT.PBP"
使用 PSP-GCC
[edit | edit source]Makefile 会自动执行构建过程。以下是它执行的基本操作。如果在 Linux 上,需要转换 -I 和 -L 开关,因为它们指向典型的 Windows 安装目录。要始终正确获取此路径,可以使用名为 psp-config 的工具。尝试使用 psp-config -p 并将它追加到 /include 以用于 -I,并将它追加到 /lib 以用于 -L。
psp-gcc -O0 -g3 -Wall -I. -IC:/pspsdk/psp/sdk/include -L. -LC:/pspsdk/psp/sdk/lib -D_PSP_FW_VERSION=150 -c *.c -lpspdebug -lpspdisplay -lpspge -lpspctrl -lpspsdk -lc -lpspnet -lpspnet_inet -lpspnet_apctl -lpspnet_resolver -lpsputility -lpspuser -lpspkernel
psp-gcc -O0 -g3 -Wall -I. -IC:/pspsdk/psp/sdk/include -L. -LC:/pspsdk/psp/sdk/lib -D_PSP_FW_VERSION=150 -o raw_eboot.elf *.o -lpspdebug -lpspdisplay -lpspge -lpspctrl -lpspsdk -lc -lpspnet -lpspnet_inet -lpspnet_apctl -lpspnet_resolver -lpsputility -lpspuser -lpspkernel
psp-fixup-imports raw_eboot.elf
mksfo 'XMB_TITLE' PARAM.SFO
psp-strip raw_eboot.elf -o strip_eboot.elf
pack-pbp EBOOT.PBP PARAM.SFO NULL NULL NULL NULL NULL strip_eboot.elf NULL
- 将所有文件编译成对象
- 将所有对象和库链接到 .elf 文件
- 为 XMB 使用创建 .SFO 文件
- 剥离 .elf 文件以缩小尺寸并减少冗余
- 创建一个 EBOOT.PBP 文件,其中包含额外的 XMB 信息(参见 pack-pbp --help)
测试
[edit | edit source]方法 1
- 导航到 EBOOT.PBP 所在的位置
- 通过命令行或终端启动 PPSSPP,提供绝对路径
方法 2
- 打开 PPSSPP
- 将 EBOOT.PBP 拖放到 PPSSPP 中
方法 3(仅限 Windows?)
- 将 EBOOT.PBP 拖放到 PPSSPP 可执行文件上
方法 4
- 打开 PPSSPP
- 转到文件
- 转到加载
- 查找并打开 EBOOT.PBP
工作示例
[edit | edit source]- main.c : http://pastebin.com/UzGLVRxB
- callback.c : http://pastebin.com/Atr5ymDV
- callback.h : http://pastebin.com/5buidb9R
- 使用上面的 Makefile
- 您也可以用汇编语言编写:main.s callback.s