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