iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 19
0
Software Development

從讀遊戲原始碼學做連線遊戲系列 第 19

Day 19 - 實作練習 - Server 雛形建立

既然已經決定好了 Server 和 Client 的方向,我們就先從伺服器端開始著手製作。至少要先有一個能提供基本操作的雛形,才能夠讓後續客戶端相對容易測試。

專案結構

在 Ruby on Rails 裡面有一個蠻經典的設計哲學「慣例優於設定」因此我們想要讓其他人快速地了解並且上手這個專案,最快的方法就是直接參考 Ruby on Rails 的結構去規劃,勁量避免使用自己的架構這樣就可以減少其他人在溝通跟學習上的時間。

因此目錄架構大致上會是像這樣的感覺:

├── Gemfile
├── Gemfile.lock
├── app/
│   ├── controllers/
│   ├── models/
│   ├── network/
│   │   └── protocol/
│   └── servers/
├── bin/
├── config/
└── db/
    └── migrations/

把主要的程式放到 app 裡面,可執行的腳本則是放在 bin 中,設定相關的在 config 裡面以及跟資料庫相關的資訊放到 db 內,基本上結構跟 Ruby on Rails 是幾乎一樣的。其他人在閱讀或者了解專案的過程中,就能夠更快的搞懂跟找到需要的檔案。

Gemfile

在現今的軟體開發大多會使用非常多套件輔助,因此初始化專案的同時必要的是先將套件管理的工具設定好。我們可以透過 bundle init 這個指令在專案初始化一個空的 Gemfile 出來。

如果沒有這個指令的話,可以用 gem install bundler 安裝。

產生後的檔案應該會類似這樣:

# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

# gem "rails"

我們接著將需要的套件定義上去:

# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

# Server
gem 'faye-websocket'
gem 'puma'
gem 'rack'

# Utils
gem 'rb-fsevent'
gem 'zeitwerk'

# Database
gem 'sqlite3'
gem 'sequel'

我自己會習慣把類似的套件分在同一個類別中,這樣會有助於之後替換套件或者其他人了解專案使用了哪些工具。習慣再好一點的會加上版本的限制,像是 ~> 2.0 這樣,確保套件一定會在 2.0 ~ 2.x 之類的版本,不會裝到 3.0 以上的版本,如果專案不小心遺失了 Gemfile.lock 儲存的版本資訊,至少還能恢復到接近的版本。

定義完畢之後,我們可以利用 bundle install 這個指令來把需要的套件安裝到電腦裡面。

主物件

在 Rails 中,假設你有一個專案叫做 SimpleRPG 那麼 Rails 是會幫你定義一個叫做 SimpleRPG::Application 的物件繼承 Rails::Application 用來做整個專案的基礎,我們也用類似的邏輯建立一個主物件來幫助我們初始化整個伺服器。

# config/application.rb

# frozen_string_literal: true

require 'rubygems'
require 'bundler'
require 'singleton'

# 透過 Bundler 自動載入套件
Bundler.require

class SimpleRPG
  include Singleton

  class << self
    def run(server, options = {})
      puts "You are starting #{server.capitalize} server with #{options}"
    end
  end
end

我們先搭建一個 Singleton 的物件,並且提供一個 .run 的方法讓我們可以透過這個方法將遊戲起動。因為在不同情境下會需要運行不同的伺服器,因此接收的參數主要是要啟動哪個伺服器,以及伺服器的設定(像是使用哪個 Port 等等)

前面有提過 Singleton 物件的用途,在這個情境下我們預期遊戲伺服器應該是只會開啟單一一個的。

啟動指令

我們現在有了主物件就需要可以執行起來測試看看,因此我們先在 bin/ 目錄下面增加一個 serve 指令來執行這個主物件。

# bin/serve

#!/usr/bin/env ruby

# frozen_string_literal: true

# 將專案根目錄設定成 require 查詢目錄
$LOAD_PATH.unshift("#{File.absolute_path('../..', __FILE__)}")

require 'optparse'

# 引用主物件
require 'config/application'

# 儲存指令設定值
options = {}
server = ARGV.shift unless ARGV[0]&.start_with?('-')

# 利用 Ruby 的 OptionParser 解析指令
OptionParser.new do |opts|
  opts.banner = 'Usage: serve SERVER [options]'

  opts.on('-p', '--port=PORT', 'Server Port') do |port|
    options[:Port] = port&.to_i
  end

  puts opts.help unless server
end.parse!

SimpleRPG.run(server, options) if server

處理完畢後我們需要先做 chmod +x bin/serve 才能讓這個指令擁有執行的權限,接著就可以嘗試用 bin/serve mapbin/serve map -p 9000 來看看指令執行的差異。

在這裡我們需要先將專案目錄導入,讓他可以直接用 require 'config/application 的方式被讀取,接下來利用 Ruby 內建的 OptionParser 幫我們處理像是 -p 9000 或者 --port=9000 這類參數的設定值,這樣我們就能夠將要啟動的伺服器以及選項傳入我們的主物件進行後續的處理。

我的個人部落格是弦而時習之平常會把自己發現的一些新技巧紀錄在上面,也歡迎大家來逛逛。


上一篇
Day 18. 實作練習 - 準備與規劃
下一篇
Day 20 - 實作練習 - WebSocket 伺服器
系列文
從讀遊戲原始碼學做連線遊戲33

尚未有邦友留言

立即登入留言