touhou-project online
Intro
東方project是一個典型的2d射擊遊戲(STG),這裏我要實現的是一個簡單的雙人聯機版 東方project 遊戲,內容涵蓋客戶端的開發和服務端的開發,主要目的是實踐網絡遊戲的同步。
源代碼倉庫託管於gitee
貼圖資源是來自網上下載的《東方地靈殿》圖集,然後自己用PS切了切,這裏給張地靈殿的遊戲截圖。
服務器架構
登錄和房間
不需要嚴格意義上的賬戶系統,所以這方面的通信操作僅僅是客戶端發出申請,服務器提供數據而已。
服務器操作 | 客戶端操作 |
---|---|
登記客戶端連接,返回玩家id | 連接服務器,申請玩家id |
檢查房間狀態,返回成功與否 | 退出/進入/創建/隨機 房間 |
是否所有玩家都已經申請開始遊戲,如果是則開始遊戲 | 申請開始遊戲 |
遊戲操作和網絡同步
採用幀鎖定同步機制,初步決定是使用嚴格幀鎖定,這裏是嚴格幀鎖定相關的參考資料
對於這個流程,在開始實現服務端的時候會再做分析
基本思路是這樣的,玩家操作先發送給服務器,等到收到服務器返回的關鍵幀後,再執行操作。
關鍵在於,如果不收到服務器提供的關鍵幀,則遊戲要暫停,等待服務器關鍵幀抵達後才繼續進行。
序號 | 服務器操作 | 客戶端操作 |
---|---|---|
1 | 關鍵幀計時器開始執行 | 控制幀計時器開始執行 |
2 | 進入關鍵幀如果已經收到所有客戶端的控制幀,則繼續下一步,否則回到上一步 | - |
3 | 整合控制幀,向所有客戶端廣播關鍵幀,重啓關鍵幀計時 | 收到關鍵幀 |
4 | - | 播放遊戲,使用關鍵幀提供的控制幀作爲玩家輸入 |
5 | - | 進入客戶端控制幀 |
6 | - | 整合控制幀間的玩家輸入,鍵盤狀態,作爲控制幀發送給服務器 |
7 | 收到控制幀 | 如果沒有收到關鍵幀,發送完控制幀後繼續等待 |
遊戲的操作很簡單,這裏可以給出所有的操作類型
操作 | 鍵位定義 |
---|---|
上下左右八方向移動 | 方向鍵 |
射擊鍵 | z |
減速 | x |
符卡 | c |
客戶端需要傳遞的主要就是鍵盤的狀態,按下還是鬆開,這樣子。
服務器接口
有了流程,那麼可以定義出服務器的接口了
接口 | 描述 | 參數 | 結果 |
---|---|---|---|
login | 申請玩家id,也可以視作登錄 | 無 | {token: string} |
curr-room | 當前所處的房間 | 無 | {room_id: number} |
join-room | 加入房間,需要提供要加入的房間id | room_id | {result: boolean} |
new-room | 創建房間 | 無 | {room_id: number} |
rand-room | 隨機加入房間,如果沒有房間,則返回空 | 無 | {room_id: number or null} |
quit-room | 退出房間,理論上應該不會出現錯誤 | 無 | {result:boolean} |
start | 申請開始遊戲,所有人都發出申請開始遊戲後,則遊戲開始 | 無 | {result: true} |
cancel-start | 取消申請開始遊戲 | 無 | {result: true} |
控制幀結構
// typescript 編寫的示例
// 也可以用其他方式編寫,比如 protobuf 啊
// 然後拿 C++ 或者 rust 寫服務端也沒問題
// 不過爲了開發速度,所以先拿 typescript 和 websocket 寫個原型
interface ICtrlFrame {
// 這是個 tuple
// 第一個number表示在x軸上的移動,負數表示向左,正數表示向右
// 第二個表示在y軸上的移動,負數表示向下,正數表示向上
motion:[number,number];
// 射擊鍵的按壓狀態
fire: boolean;
// 低速鍵的按壓狀態
slow: boolean;
// 符卡鍵的按壓狀態
spell: boolean;
}
關鍵幀結構
interface IKeyFrame {
// 關鍵幀的序號
frameIndex: number;
// 控制幀
// {[index:number]: ICtrlFrame} 這個寫法是 ts 特有的
// 用起來相當於其他語言的 HashTable 之類的,舉個例子來說
// 差不多像是 C++ 的 std::map<int, ICtrlFrame>
// Python 的 dict 這樣
// 順便一提 Python3 的 type annotation 可以指定 Dict[int,ICtrlFrame] 這樣的類型
// import typing
// ctrl: Dict[int,ICtrlFrame] = {} # 像這樣
// 但是沒有強類型檢查
ctrl: {[index:number]: ICtrlFrame};
}