既然已經決定好了 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 是幾乎一樣的。其他人在閱讀或者了解專案的過程中,就能夠更快的搞懂跟找到需要的檔案。
在現今的軟體開發大多會使用非常多套件輔助,因此初始化專案的同時必要的是先將套件管理的工具設定好。我們可以透過 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 map
和 bin/serve map -p 9000
來看看指令執行的差異。
在這裡我們需要先將專案目錄導入,讓他可以直接用 require 'config/application
的方式被讀取,接下來利用 Ruby 內建的 OptionParser 幫我們處理像是 -p 9000
或者 --port=9000
這類參數的設定值,這樣我們就能夠將要啟動的伺服器以及選項傳入我們的主物件進行後續的處理。
我的個人部落格是弦而時習之平常會把自己發現的一些新技巧紀錄在上面,也歡迎大家來逛逛。