PSP 开发/用户输入
本文建立在上一篇文章的基础上,Hello World 应用。建议您从该文章开始并理解其中的代码。本节的具体目标是介绍如何使用 PSP 的用户输入(UI) 系统。
PSPSDK 库提供了硬件和程序之间的接口。此接口查询硬件并返回结果。然后使用AND运算符(&)和表示按钮的数字来检查此结果。由于位对齐的方式,返回的结果可以被视为布尔位,因此是 AND 运算符的目的。此逻辑的一个示例是 1010 可以是按钮 1 被按下,按钮 2 未被按下,按钮 3 被按下,按钮 4 未被按下。PSP 系统有两种检查状态的方法:锁存方法和状态方法。
UI 检查的典型流程是
- 初始化采样
- 查询硬件以获取操纵杆信息
- 查询硬件以获取按键信息
- 利用按键和操纵杆信息
本节详细介绍了所需文件结构的补充内容。这些更改是强制性的,不应跳过。虽然可以重命名,但可能会导致头痛。
在开始之前,从上一篇文章中复制 hello_world 项目文件夹并将其重命名为 user_input 将节省时间。丢弃目标文件、EBOOT.PBP 和 elf 文件是安全的 - 它们目前没有用。丢弃项目/common 中的目标文件没有必要。
更新 Makefile 是第一步。更改项目以适应我们的新环境是一种良好的做法。
- 将 TARGET 更改为 user_input
- 将 PSP_EBOOT_TITLE 更改为 UserInput
- 在 OBJS 下有 main.o 和 ../common/callback.o
- 在 main.c 内部,将 PSP_MODULE_INFO() 调用中的字符串切换为 UserInput
- 清空 main.c 中除以下基本模板以外的所有内容
main.c
int main(int argc, char** argv)
{
// basic init
setupExitCallback();
pspDebugScreenInit();
while (isRunning()) {
pspDebugScreenSetXY(0, 0);
// empty
sceDisplayWaitVblankStart();
}
// close out
sceKernelExitGame();
return 0;
}
用户输入是常见模块的完美示例,因为另一篇文章已经进行了说明。这两个文件将在所有需要通过控制器进行用户输入的应用程序中使用。
- 导航到 common 文件夹
- 创建一个名为 ui.c 和 ui.h 的编译单元
- 在 ui.c 中包含 ui.h
- 在 main.c 中包含 ui.h
- 转到项目 Makefile 并将 ../common/ui.o 附加到 OBJS 变量
以下是这些文件应包含的内容。
ui.c
#include "ui.h"
ui.h
#ifndef UI_H
#define UI_H
#endif
接下来的任务是实现方法。任何与控制器相关的内容都需要包含 pspctrl.h。获取硬件信息后,下一步是处理信息,利用 ui 编译单元。
术语
- 按键保持 - 按键按下/释放每次状态切换触发一次,即按键按下 - 按键抬起
- 按键锁存 - 按住按键会每帧触发恒定动作,即按键按下 - 按键按住
可以获取两组信息 - 保持或锁存。锁存方法具有保持方法信息所拥有的按键信息。添加保持方法只会带来操纵杆和时间戳信息。本文将演示两种方法。
- 在 pspctrl.c 中包含 pspctrl.h
- 添加一个变量(结构体)SceCtrlData data
- 添加函数 int getJX() 用于操纵杆 X
- 添加函数 int getJY() 用于操纵杆 Y
- 添加函数 void pollPad() 用于查询硬件
ui.c
#include <pspctrl.h>
static SceCtrlData data;
int getJX() {
}
int getJY() {
}
void pollPad() {
}
ui.h
int getJX();
int getJY();
void pollPad();
pollPad() 的目的是每帧调用一次查询硬件。在尝试检查用户输入之前必须查询硬件,并确保没有使用旧数据。用于将数据从硬件读入 SceCtrlData 的函数是 sceCtrlReadBufferPositive(),它接受 SceCtrlData 的内存地址和所需的缓冲区读取量。只需要读取一次。有两种不同的硬件轮询函数:读取和窥视。sceCtrlPeekBufferPositive() 不会弹出用户输入信息。使用 sceCtrlPeekBufferPositive(),可以在多个编译单元中分别检查用户输入,每次都轮询硬件。窥视和读取函数的一种用途是实现撤消功能,或在游戏动作中根据按钮顺序进行依赖。
ui.c
void pollPad() {
sceCtrlReadBufferPositive(&data, 1);
}
SceCtrlData 结构体具有值:int TimeStamp、int Buttons、int Lx 和 int Ly。TimeStamp 用于告知当前读取的帧,本文未利用它。Buttons 是一个整数,包含任何当前按下的按键的位布尔值,本文未利用它,因为利用了锁存方法。Lx 和 Ly 变量分别与 getJX() 和 getJY() 函数配对,因为这些函数公开了操纵杆信息。LX 和 Ly 是带符号的 char 长度数字,不保证完全处于中间。中间是 128 X 和 128 Y。
ui.c
int getJX() {
return data.Lx;
}
int getJY() {
return data.Ly;
}
根据程序的不同,按键锁存是必不可少的组件。此方法使用一个名为 SceCtrlLatch 的不同结构体。SceCtrlData 将是 ui 编译单元的另一个静态成员,pollLatch() 将从硬件中读取。SceCtrlLatch 累积地存储关于按钮的信息,作为 SceCtrlData 中的位布尔值数字 - 例如,两个按钮按下可能看起来像 1100。
ui.c
#include <pspctrl.h>
static SceCtrlLatch latch;
void pollLatch() {
}
ui.h
void pollLatch();
虽然 sceCtrlReadBufferPositive() 用于保持,但 sceCtrlReadLatch() 用于锁存。锁存只需要一个参数 - 对锁存数据结构的引用。锁存只有几个成员:uiMake、uiBreak、uiPress 和 uiRelease。
void pollLatch() {
sceCtrlReadLatch(&latch);
}
为了检查按键是否被按下,需要将位布尔值与要检查的按键进行 AND 运算。如果结果为 1,则表示正在检查的按键处于按下状态,否则为 0,表示处于抬起状态 - 口语上说,"按键是否按下" 为 true,"按键是否抬起" 为 false。
观察 sceCtrlReadBufferPositive()(二进制)模式
- 0100 // 位布尔值
- 0001 & // AND 按键-0001
- 0000 // 按键-0001 未按下
- 0101 // 位布尔值
- 0001 & // AND 按键-0001
- 0001 // 按键-0001 已按下
需要一个辅助函数来检查保持方法输入,因为该结构体在 main.c 中不可见。需要访问 SceCtrlLatch 成员。在 C 语言中,0 为 false,1 为 true,用于布尔检查。锁存具有 uiPress 和 uiRelease 字段。uiPress 是位布尔值,表示当前按下的按键。uiRelease 是 -1 - uiPress。目前只有 uiPress 相关。该函数请求一个按键,并将其与当前按住的按键进行 AND 运算,以生成按键保持方法。一个合适的函数将是 isKeyHold()。
ui.c
int isKeyHold(int key) {
return latch.uiPress & key;
}
ui.h
int isKeyHold(int key); // returns 1(true) if key is down
对于闩锁方法,使用 uiMake 和 uiBreak 字段。当任意数量的键同时按下时,uiMake 将从 0 更新到仅在那一帧按下所有键的位布尔值。当任意数量的键同时释放时,uiBreak 将从 0 更新到仅在那一帧释放所有键的位布尔值。如果键 1011 同时按下,uiMake 将为 1011,在下一帧,如果没有任何其他键按下,则为 0000。如果在下一帧仅释放键 0010,uiBreak 将为 0010,并且在下一帧,如果没有任何其他键释放,则为 0000。如果在下一帧同时释放键 1001,uiBreak 将为 1001。适当的函数将是 isKeyDown() 和 isKeyUp()。
ui.c
int isKeyDown(int key) {
return (latch.uiMake & key);
}
int isKeyUp(int key) {
return (latch.uiBreak & key);
}
ui.h
int isKeyDown(int key);
int isKeyUp(int key);
一个简单的示例任务是当按下某个键时,它将打印一条消息 - 说明按下的键是处于按下状态,在一帧内是处于释放状态,还是在一帧内是处于按下状态。以下是可检查的可能键的列表。虽然列表并不完全详尽,但某些键只能在内核模式下检查。强烈建议避免使用内核模式。
- PSP_CTRL_SELECT
- PSP_CTRL_START
- PSP_CTRL_UP
- PSP_CTRL_RIGHT
- PSP_CTRL_DOWN
- PSP_CTRL_LEFT
- PSP_CTRL_LTRIGGER
- PSP_CTRL_RTRIGGER
- PSP_CTRL_TRIANGLE
- PSP_CTRL_CIRCLE
- PSP_CTRL_CROSS
- PSP_CTRL_SQUARE
需要调用两个函数,它们会编辑采样周期和采样模式。这两个函数是 sceCtrlSetSamplingCycle() 和 sceCtrlSetSamplingMode()。在进行任何用户输入检查之前,需要在程序开头,在主循环之前调用这两个函数。sceCtrlSetSamplingCycle() 接收一个 int,通常为 0。sceCtrlSetSamplingMode() 接收一个枚举类型,通常为 PSP_CTRL_MODE_ANALOG。
main.c
sceCtrlSetSamplingCycle(0);
sceCtrlSetSamplingMode(PSP_CTRL_MODE_ANALOG);
为了演示这些函数,从硬件中轮询当前的键状态。打印十字键是处于按下状态还是处于释放状态,是否按住圆圈键,以及当前的操纵杆轴位置。
main.c
while (isRunning()) {
sceDisplayWaitVblankStart();
pspDebugScreenClear();
pspDebugScreenSetXY(0, 0);
pollPad();
pollLatch();
if(isKeyDown(PSP_CTRL_CROSS))
printf("Cross is down!\n");
if(isKeyUp(PSP_CTRL_CROSS))
printf("Cross is up!\n");
if(isKeyHold(PSP_CTRL_CIRCLE))
printf("Circle is down!\n");
printf("%d,%d", getJX(), getJY());
}
TARGET = user_input
OBJS = main.o ../common/callback.o ../common/ui.o
INCDIR =
CFLAGS = -O2 -G0 -Wall
CXXFLAGS = $(CFLAGS) -fno-exceptions -fno-rtti
ASFLAGS = $(CFLAGS)
LIBDIR =
LIBS =
LDFLAGS =
EXTRA_TARGETS = EBOOT.PBP
PSP_EBOOT_TITLE = UserInput
PSPSDK=$(shell psp-config --pspsdk-path)
include $(PSPSDK)/lib/build.mak
通过将 Windows 命令行或终端打开到项目目录并执行 make EBOOT.PBP 来构建。在 PPSSPP 中启动 EBOOT.PBP。在输入成为可能之前,PPSSPP 可能需要将控制器映射配置到键盘。
- main.c : http://pastebin.com/RefSXENc
- callback.c : http://pastebin.com/FUtADHFr
- callback.h : http://pastebin.com/aRXbja2r
- ui.c : http://pastebin.com/pcNJG7TF
- ui.h : http://pastebin.com/kxqZYMNb