《球球大作戰》原始碼解析:伺服器與客戶端架構
系列文章
《球球大作戰》原始碼解析——(1)執行起來
《球球大作戰》原始碼解析:伺服器與客戶端架構
《球球大作戰》原始碼解析:移動演算法
《球球大作戰》原始碼解析(6):碰撞處理
《球球大作戰》原始碼解析(7):遊戲迴圈
《球球大作戰》原始碼解析(8):訊息廣播
鑑於agar.io型別遊戲的火爆場面,一些公司紛紛效仿,一時間出現各種《XX大作戰》型別的遊戲。出於學習的目的,亦是做些技術和方案儲備,接下來會有大概10篇文章,分析下面這款使用nodejs編寫的開源“球球大作戰”。由於該遊戲採用服務端運算、客戶端顯示的方式,服務端的邏輯處理是該原始碼的重點,故而系列文章主要針對服務端。通過這套原始碼,可以學習到“一種基於nodejs的簡單伺服器實現方法”“一種簡單的服務端物理邏輯的實現方式”“一種基於redis pub/sub的跨服設計思想”“nodejs語法、框架及其使用方式”等內容。
系列文章將會分析huytd/agar.io-clone的原始碼,這是一套簡約而不簡單的Agar.IO實現。該專案使用NodeJS開發,使用socket.IO作為網路通訊,使用HTML5實現客戶端。
一、執行起來
下圖為遊戲執行畫面,遊戲規則如下。
1、玩家可以移動滑鼠控制小球
2、當小球吞食場景中的食物或其他玩家控制的小球時,玩家控制的小球會變大
3、小球越大,移動速度越慢
4、小球的質量代表它的大小,質量為它吞食的食物或其他玩家的質量之和
5、遊戲目標是儘可能的吞食其他玩家,使小球變大
6、玩家剛出生時會有無敵屬性,直到它吞食食物
7、每當有玩家進入遊戲,場景中會生成3個食物
8、每當吞食食物時,場景中亦會生成一個新的食物
第一步便是要讓遊戲執行起來,只有執行起來了,才談得上後續的原始碼分析。為了“從零開始”,筆者購買Ubuntu系統的騰訊雲,新的系統幾乎沒有安裝額外軟體,一步一步安裝所需的軟體,然後將遊戲執行起來吧。筆者選用了最低一檔配置的伺服器,花費近50大洋(此處是不是應該發個求贊助的連結?)配置如下圖所示。
1、安裝nodeJs
遊戲使用nodejs開發,那就必須要安裝nodejs,可以有兩種方法安裝。
方法1:輸入sudo apt install nodejs,這是最簡單的安裝方法了。不過使用該方式安裝的程式名叫為nodejs,而不是普遍使用的node。可以使用sudo ln-s/usr/bin/nodejs/usr/bin/node建立名為node的連線,以解決這個問題。
方法2:下載原始碼、編譯、安裝。具體可以參考這篇文章在Ubuntu下安裝Node.JS的不同方式-技術◆學習|Linux.中國-開源社群(文章裡使用的node-v6.9.5要改為最新版的)
完成後,可以使用node-v檢視nodejs版本號,以驗證是否成功安裝。
2、上傳程式碼檔案
從github上下載原始碼,然後上傳到linux伺服器上。如下圖所示,筆者將原始碼上傳到/home/ubuntu/agar.io-clone-master目錄下
3、安裝npm
npm(node package manager)是nodejs的包管理和分發工具,一般安裝nodejs後都需要安裝該軟體,可以使用以下命令安裝:sudo apt install npm
4、安裝gulp
專案使用到了gulp,需要安裝它。gulp是一個前端構建工具,開發者可以使用它在專案開發過程中自動執行常見任務,比如複製檔案,比如替換檔案中某些字元。進入原始碼目錄,執行sudo npm install-g gulp即可安裝。
5、安裝專案所需的包檔案
進入原始碼目錄,執行npm install即可安裝專案所需包檔案。npm install會檢查當前目錄下的package.json檔案,檔案包含了專案所需的模組,npm根據該檔案的描述下載這些檔案並把模組放到./node_modules目錄下。關於package.json的格式可以參考這篇文章package.json for NPM檔案詳解
6、執行伺服器
在原始碼目錄下執行gulp run,可以看到伺服器啟動的提示資訊。
7、執行客戶端
執行瀏覽器,輸入地址即可,筆者的騰訊雲ip為139.199.179.39,由於預設配置了3000埠,所以要輸入http://139.199.179.39:3000/,即可看到如下的遊戲介面。
在筆者的試驗中,該頁面報錯,點選按鈕沒有反應。原因是src/client中的index.html最後面有這麼一句,<script src="//code.jquery.com/jquery-2.2.0.min.js"></script>,該語句用於載入jquery的,而http://code.jquery.com/jquery-2.2.0.min.js無法訪問(或國內網路訪問速度慢),導致報錯。只要換個檔案地址即可,例如改成下面這樣:
<script src="http://libs.baidu.com/jquery/1.9.0/jquery.js"></script >
執行遊戲,服務端也會列印出相應的資訊,如下圖所示。
把遊戲執行起來後,下一步就要分析下游戲的流程了。
二、程式流程
在解析原始碼之前,需要先了解該專案的程式流程,瞭解客戶端和服務端是如何執行和通訊的。本文是wiki文件Game Architecture的翻譯,以幫助讀者從大方向上了解《球球大作戰》。
程式架構
遊戲程式使用NodeJs編寫,服務端通過http://Socket.IO建立WebSocket服務並預設監聽3000號埠。程式還使用ExpressJS建立一個簡單的HTTP伺服器,它負責html頁面的顯示。index.html是遊戲主頁面,它通過Canvas渲染遊戲,通過Javascript指令碼和服務端通訊。
目錄結構該專案由3部分組成:
1、配置檔案,如package.json,config.json等等
2、客戶端程式
3、服務端程式
配置檔案package.json列出了專案所需的庫檔案,讀者只需在專案目錄下執行“npm install”即可自動安裝這些檔案。package.json的格式可以參考下面的文章:
npm package.json屬性詳解
遊戲客戶端
client資料夾裡包含了客戶端所需的程式碼,它是一個簡單的HTML檔案,該檔案會通過canvas繪製遊戲場景、聊天框等元素。
js/app.js是客戶端的邏輯程式碼,它實現了畫面渲染、網路延遲檢測、觀戰模式、聊天等功能,處理了滑鼠輸入、服務端通訊等事項。遊戲採用服務端運算模式,客戶端只是負責將服務端發來的資料顯示到螢幕上,以及接收滑鼠事件。
客戶端程式使用了requestAnimationFrame程式渲染迴圈,而不是使用setInterval,這讓遊戲有著更好的渲染效能。你可以試著修改程式碼,呼叫setInterval方法,看看低效率的渲染是個啥樣子。
(function animloop(){
requestAnimFrame(animloop);
gameLoop();
})();
to
setInterval(gameLoop, 16);
遊戲服務端
server/server.js包含了服務端的配置和邏輯處理,配置了諸如食物質量、移動速度、無敵狀態的最大質量,處理了食物顏色計算、碰撞檢測、玩家移動處理等等事項。
所有的遊戲邏輯都在服務端處理,服務端和客戶端的通訊有著下面幾個要點。
1、服務端使用list儲存玩家列表,而不是使用array,使用list儲存食物列表,而不是使用array。服務端儲存著socket列表,用於記錄所有客戶端連線。
2、之前的版本設定了一個定時器,每隔幾秒鐘就產生一些食物,但這種方法的效率不高,會延遲服務端處理速度。所有在此版本中使用了一種新的方式來產生食物,當一個玩家進入遊戲時,程式會隨機產生3個食物(可以修改配置檔案的newFoodPerPlayervariable改變該數值),當玩家吃掉一個食物時,程式會產生另外一個食物(可以修改配置檔案的respawnFoodPerPlayer改變該數值)。如果場景中的食物數量大於50(配置檔案的maxFoodCount),服務端會停止產生新食物。
客戶端服務端通訊
客戶端與服務端通訊可以分為兩個部分,分別是登入認證和遊戲內通訊。
登陸認證
當一個玩家開啟遊戲網頁,他先會看到一個輸入使用者名稱的對話方塊,點選“Play”按鈕後,客戶端發起socket連線,服務端accept連線後發出welcome協議,並把該客戶端的UserID附帶在協議中。
當客戶端收到welcome協議,它會返回附帶使用者名稱的gotit協議。
當服務端收到gotit協議,它會其它的已連線玩家廣播playerJoin協議,告訴他們有新的玩家加入。其它玩家收到該協議後,會在螢幕上繪製這個新加入的角色。
此時,對於新加入的玩家來說,遊戲剛剛開始。
遊戲內通訊
遊戲內通訊分為3個部分,分別是遊戲邏輯、聊天和Ping(測試網路延遲)。
遊戲邏輯
玩家在遊戲中會有移動、吞食食物、吞食其他玩家三種行為,這些邏輯全部由服務端運算,客戶端只是根據運算結果將影像顯示在對應的位置上。
移動
當玩家移動滑鼠,小球會朝著滑鼠的位置移動。客戶端會傳送附帶了目的地座標的playerSendTarget協議。服務端收到協議後會更新小球的運動狀態,然後向該客戶端回覆serverTellPlayerMove協議,然後傳送serverUpdateAllPlayers給其他客戶端,讓全部客戶端更新所有玩家的座標。
小球移動期間,服務端還會檢測小球是否吞食了食物,或者吞食了其他玩家。
吞食食物
服務端維持了users列表和food列表來儲存所有的小球和食物的資訊,如果小球碰到食物,服務端會執行相應的邏輯,增加小球質量、刪除列表裡的食物、產生新的食物。然後服務端廣播serverUpdateAllPlayers和serverUpdateAllFoods協議,讓客戶的更新玩家和食物。
吞食其他玩家
如果小球吞食了其他玩家的小球,服務端會比較兩者的質量和距離,質量小的被吞食。服務端會傳送RIP協議告訴質量下的玩家他死掉了,然後斷開與該玩家的連線,同時在users列表裡刪除他。還會廣播serverUpdateAllPlayers協議通知客戶端。
聊天
聊天的流程如下圖所示
當玩家在聊天框中輸入資訊並按下Enter鍵時,客戶端向服務端傳送playerChat協議,服務端收到協議後廣播serverSendPlayerChat協議。
當客戶端收到serverSendPlayerChat協議時,它會解析該協議,將聊天內容顯示到螢幕上。
Ping(延遲檢測)
網路遊戲都會實現ping機制來檢測客戶端和服務端之間的延遲,而它的實現也很簡單。
檢測開始時,客戶端會儲存當前的開始時間,然後傳送ping協議給服務端,服務端收到後,會返回pong協議。客戶端收到pong協議會計算時間差,如果時間差很大,說明網路延遲很嚴重。
願這份文件能夠協助讀者理解agar.io-clone這個專案,你還可以繼續完善這款遊戲,將它做得更好。也希望各位能夠在專案wiki中分享心得。
三、gulp工具
執行遊戲使用的命令是gulp run,agar.io-clones使用了nodejs開發,gulp是基於nodejs的一個工具,它能夠批量的做一些檔案操作。gulp run意思是執行目錄下gulpfile.js下的run任務,那麼原始碼中使用了gulp的哪些功能呢?這篇文章將會做個簡單介紹。
gulp能自動化地完成javascript/coffee/sass/less/html/image/css等檔案的的測試、檢查、合併、壓縮、格式化、瀏覽器自動重新整理、部署檔案生成,並檢測檔案變化。在實現上,gulp鑑了Unix作業系統的管道(pipe)思想,前一級的輸出,直接變成後一級的輸入。
關於gulp入門,可以參考下面的文章:
一點|gulp詳細入門教程
入門指南-gulp.js中文文件
一個最簡單的示例
要使用gulp根據,當然得先安裝它,有兩種方式安裝,對應於不同的命令引數。
全域性安裝gulp:npm install--global gulp
作為專案的開發依賴(devDependencies)安裝:npm install--save-dev gulp
現在新建一個目錄並建立一個名為gulpfile.js的檔案,在裡面編寫如下程式碼
var gulp = require('gulp');gulp.task('default', function() { // 將你的預設的任務程式碼放在這});
在目錄下執行gulp,此時程式會搜尋目錄下gulpfile.js檔案中的預設(default)任務,也就是上面程式碼中“//將你的預設的任務程式碼放在這”處的程式碼去執行。“gulp run”即表示執行名為run的任務,相關程式碼可以在專案資料夾下的gulpfile.js中看到。相關程式碼如下
gulp.task('run', ['build'], function () {
nodemon({
delay: 10,
script: './server/server.js',
cwd: "./bin/",
args: ["config.json"],
ext: 'html js css'
})
.on('restart', function () {
util.log('server restarted!');
});
});
程式碼解析
要看懂上面的程式碼,必須要了解gulp的一些API,知道“nodemon”等單詞到底是什麼意思,實現什麼功能,gulp的api可以參考下面的文章:
一點|gulp教程之gulp中文API
依賴
上面程式碼中的“gulp.task('run',['build'],function(){}”意為run依賴於build,當執行gulp run時,程式會先執行build任務,再執行run任務。
nodemon
先看看nodemon,詳細的解釋可以參考gulp-nodemon。
nodemon是一個工具,用於專案程式碼發生變化時可以自動重啟,nodemon本意時檢測專案變化的,對專案做監控的。重啟只是它的一個功能。在上面的程式碼中,相當於執行./server/server.js這個檔案。而這個檔案其實是build任務中生成的。
build任務
接下來看看build任務是什麼樣子的,會發現build任務依賴於build-client、build-server、test、todo這4個任務,也就是說,需要按順序先執行這4個任務,才會執行build。此時我們會發現,程式碼的執行流程是build-client、build-server、test、todo、run
gulp.task('build',['build-client','build-server','test','todo']);
build-client任務
build-client處理了客戶端程式碼的建立,它用到了uglify、webpack和babel。
其中uglify表示壓縮javascript檔案,減小檔案大小(參見一點|gulp教程之gulp-uglify)
webpack表示模組打包,它能幫我們把本來需要在服務端執行的JS程式碼,通過模組的引用和依賴打包成前端可用的靜態檔案(參考《nodejs+gulp+webpack基礎實戰篇》課程筆記(三)--webpack篇-亡命小卒-部落格園)
babel是一個JavaScript轉換編譯器,它可以將ES6(下一代JavaScript規範,新增了一些新的特性和語法)轉換成ES5(可以在瀏覽器中執行的程式碼)。這就意味你可以在一些暫時還不支援某些ES6特性的瀏覽器引擎中,使用ES6的這些特性。比如說,class和箭頭方法。
pipe表示管道,下面的程式碼是指將原始檔(.src)“src/client/js/app.js”通過uglify方法壓縮,然後將壓縮後的結果通過webpack打包,然後通過babel做相容性,最後通過將檔案存入dest指定的目錄下“bin/client/js/”
gulp.task('build-client', ['lint', 'move-client'], function () { return gulp.src(['src/client/js/app.js']) .pipe(uglify()) .pipe(webpack(require('./webpack.config.js'))) .pipe(babel({ presets: [ ['es2015', { 'modules': false }] ] })) .pipe(gulp.dest('bin/client/js/'));});
webpack()方法的引數是“require('./webpack.config.js') ”“ ./webpack.config.js”,該檔案的內容如下,它是打包的配置檔案。
module.exports = {
entry: "./src/client/js/app.js",
output: {
path: require("path").resolve("./src/bin/client/js"),
library: "app",
filename: "app.js"
},
module: {
loaders: [
{
test: /\.jsx?$/,
exclude: /(node_modules|bower_components)/,
loader: 'babel'
}
]
}
};
“build-client”依賴於“lint”和“move-client”,先要完成這兩個任務,程式才會執行“build-client”任務。
lint任務
“lint”任務如下所示,它使用了jshint方法。jshint是用來檢測javascript的語法錯誤的。如果有錯誤,就報告fail。
gulp.task('lint', function () {
return gulp.src(['**/*.js', '!node_modules/**/*.js', '!bin/**/*.js'])
.pipe(jshint({
esnext: true
}))
.pipe(jshint.reporter('default', { verbose: true}))
.pipe(jshint.reporter('fail'));
});
move-client任務
“build-client”還依賴於“move-client”程式碼如下,它只是移動一些檔案
gulp.task('move-client', function () {
return gulp.src(['src/client/**/*.*', '!client/js/*.js'])
.pipe(gulp.dest('./bin/client/'));
});
build-server任務
build-server任務比較簡單,它也是複製下檔案
gulp.task('build-server', ['lint'], function () {
return gulp.src(['src/server/**/*.*', 'src/server/**/*.js'])
.pipe(babel())
.pipe(gulp.dest('bin/server/'));
});
test任務
build任務依賴於build-client、build-server、test和todo任務,在建了客戶端和服務端檔案後,自然需要對它測試一下,test任務呼叫了mocha方法,它是一個測試方法。
gulp.task('test', ['lint'], function () {
gulp.src(['test/**/*.js'])
.pipe(mocha());
});
todo任務
todo任務呼叫了todo方法,該方法會收集符合“src/**/*js”匹配符的檔案資訊,生成一個名為TODO.md的檔案。
gulp.task('todo', ['lint'], function() { gulp.src('src/**/*.js') .pipe(todo()) .pipe(gulp.dest('./'));});
生成的TODO.md如下圖所示。
由於實際執行的檔案在是bin/目錄下,如果修改了原始檔,需要重新執行gulp run才能生效。
四、Websocket
執行服務端後,玩家只要開啟瀏覽器,輸入地址和埠,就可以看到遊戲畫面。這就意味著,遊戲服務端開了個http伺服器。Node.js標準庫提供了http模組,其中封裝了一個高效的HTTP伺服器和一個簡易的HTTP客戶端。http.Server是一個基於事件的HTTP伺服器,它的核心由Node.js下層C++部分實現,而介面由JavaScript封裝,兼顧了高效能與簡易性。http.request則是一個HTTP客戶端工具,用於向HTTP伺服器發起請求。關於http服務端的入門,可以參考下面教程。
Node.js學習(11)----HTTP伺服器與客戶端-推酷
安裝http包
使用http模組,必須先安裝它,執行npm install http命令安裝即可。
顯示Html文字
新建一個js檔案,然後輸入如下的程式碼。通過require('http').Server建立一個http伺服器,“http.listen”表示開啟監聽,如下程式碼是監聽3001埠,監聽成功後會在螢幕中列印出“[DEBUG]Listening”。http.on('request'function(){……})表示當服務端收到客戶端的請求時做出怎樣的處理,這裡向客戶端返回html資訊。
var http = require('http').Server()
http.on('request',function(req,res){
console.log('[DEBUG] on request ' );
res.writeHead(200,{'Content-Type':'text/html'});
res.write('<h1>Node.js</h1>');
res.end('<p>HelloWorld</p>');
});
http.listen(3001, function() {
console.log('[DEBUG] Listening ' );
});
執行指令碼,然後用瀏覽器開啟3001埠,即可看到html文字。
用express顯示Html檔案
Express是一個基於Node.js平臺的web應用開發框架,可以使用它指定要顯示的網頁檔案。在使用之前需要使用npm install express命令安裝express。
新建js檔案填入下面的程式碼,除了建立http伺服器外,還使用express指定了網頁目錄“__dirname+'/'”,即程式碼檔案的同一目錄下。
var express = require('express');
var app = express();
var http = require('http').Server(app)
var io = require('socket.io')(http);
app.use(express.static(__dirname + '/'));
console.log("hehe");
http.listen(3001, function() {
console.log('[DEBUG] Listening ' );
});
在同一目錄下新建index.html,輸入下面的文字。
<html>
<head>
<title>Ssocket</title>
</head>
<body>
<P>測試</P>
</body>
</html>
執行服務端,用瀏覽器開啟頁面,將會看到如下網頁。
WebSocket介紹
談到Web實時推送,就不得不說WebSocket。在WebSocket出現之前,很多網站為了實現實時推送技術,通常採用的方案是輪詢(Polling)和Comet技術,Comet又可細分為兩種實現方式,一種是長輪詢機制,一種稱為流技術,這兩種方式實際上是對輪詢技術的改進,這些方案帶來很明顯的缺點,需要由瀏覽器對伺服器發出HTTP request,大量消耗伺服器頻寬和資源。面對這種狀況,HTML5定義了WebSocket協議,能更好的節省伺服器資源和頻寬並實現真正意義上的實時推送。
WebSocket協議本質上是一個基於TCP的協議,它由通訊協議和程式設計API組成,WebSocket能夠在瀏覽器和伺服器之間建立雙向連線,以基於事件的方式,賦予瀏覽器實時通訊能力。既然是雙向通訊,就意味著伺服器端和客戶端可以同時傳送並響應請求,而不再像HTTP的請求和響應。
具體可以參考下面的文章
使用Node.js+Socket.IO搭建WebSocket實時應用-OPEN開發經驗庫
WebSocket簡單例項
下面通過一個簡單的例子介紹WebSocket的使用方法,在安裝WebSocket後編寫如下的程式碼和html檔案。當客戶端發起連線(connection)後,它會列印出“A user connected!”
var express = require('express');
var app = express();
var http = require('http').Server(app)
var io = require('socket.io')(http);
app.use(express.static(__dirname + '/'));
console.log("hehe");
io.on('connection', function (socket) {
console.log('A user connected!', socket.handshake.query.type);
})
http.listen(3001, function() {
console.log('[DEBUG] Listening ' );
});
html程式碼如下所示,頁面中會有一個按鈕,當點選按鈕時,會通過io.connect連線服務端
<html>
<head>
<title>Ssocket</title>
http://139.199.179.39:3001/socket.io/socket.io.js</a>">
</head>
<body>
<P>測試</P>
<input type="button" id="btn" value="click" />
<script type="text/javascript">
var oBtn = document.getElementById('btn');
oBtn.onclick = function(){
var socket = io.connect('http://139.199.179.39:3001/');
alert("send");
};
</script>
</body>
</html>
執行程式,點選客戶端上的按鈕,服務端會顯示“A user connected!”
收發資訊
客戶端和服務端可要相互通訊,在下面的例子中,網頁上有connect和send兩個按鈕,點選send按鈕後,會傳送login協議,服務端收到login協議後,會列印客戶端傳來的資訊。
var express = require('express');
var app = express();
var http = require('http').Server(app)
var io = require('socket.io')(http);
app.use(express.static(__dirname + '/'));
console.log("hehe");
io.on('connection', function (socket) {
console.log('A user connected!', socket.handshake.query.type);
socket.on('login', function (data) {
console.log(data);
});
})
http.listen(3001, function() {
console.log('[DEBUG] Listening ' );
});
<html>
<head>
<title>Socket</title>
http://139.199.179.39:3001/socket.io/socket.io.js</a>">
</head>
<body>
<P>測試</P>
<input type="button" id="btn1" value="connect" />
<input type="button" id="btn2" value="send" />
<script type="text/javascript">
var oBtn1 = document.getElementById('btn1');
oBtn1.onclick = function(){
socket = io.connect('http://139.199.179.39:3001/');
alert("connect");
};
var oBtn2 = document.getElementById('btn2');
oBtn2.onclick = function(){
socket.emit('login', { name: 'LPY' });
alert("send");
};
</script>
</body>
</html>
執行程式,點選按鈕,服務端將會顯示客戶端login協議傳入的使用者名稱“LPY”,如下圖所示。
客戶端
服務端
客戶端回顯
在下面的程式碼中,服務端收到客戶端的login協議後會恢復客戶端loginBack協議,客戶端收到loginBack協議後會彈出對話方塊。
var express = require('express');
var app = express();
var http = require('http').Server(app)
var io = require('socket.io')(http);
app.use(express.static(__dirname + '/'));
console.log("hehe");
io.on('connection', function (socket) {
console.log('A user connected!', socket.handshake.query.type);
socket.on('login', function (data) {
console.log(data);
socket.emit('loginBack', { result: 'success' });
});
})
http.listen(3001, function() {
console.log('[DEBUG] Listening ' );
});
<html>
<head>
<title>Socket</title>
http://139.199.179.39:3001/socket.io/socket.io.js</a>">
</head>
<body>
<P>測試</P>
<input type="button" id="btn1" value="connect" />
<input type="button" id="btn2" value="send" />
<script type="text/javascript">
var oBtn1 = document.getElementById('btn1');
oBtn1.onclick = function(){
socket = io.connect('http://139.199.179.39:3001/');
alert("connect");
socket.on('loginBack', function (data) {
alert(data.result);
});
};
var oBtn2 = document.getElementById('btn2');
oBtn2.onclick = function(){
socket.emit('login', { name: 'LPY' });
alert("send");
};
</script>
</body></html>
執行程式,結果如下圖所示。
還是放個廣告吧,筆者出版的一本書《Unity3D網路遊戲實戰》充分的講解怎樣開發一款網路遊戲,特別對網路框架設計、網路協議、資料處理等方面都有詳細的描述,相信會是一本好書的。
知乎專欄:https://zhuanlan.zhihu.com/pyluo
相關文章
- 《球球大作戰》原始碼解析(7):遊戲迴圈原始碼遊戲
- 《球球大作戰》原始碼解析(6):碰撞處理原始碼
- 《球球大作戰》原始碼解析——(9)訊息處理原始碼
- 《球球大作戰》原始碼解析(8):訊息廣播原始碼
- 《球球大作戰》原始碼解析:移動演算法原始碼演算法
- 《球球大作戰》原始碼解析——(1)執行起來原始碼
- 《球球大作戰》優化之路(上)優化
- 《球球大作戰》優化之路(下)優化
- 羽毛球群檔案--客戶手冊
- 支付寶客戶端架構解析:iOS 客戶端啟動效能優化初探客戶端架構iOS優化
- Redis原始碼剖析——客戶端和伺服器Redis原始碼客戶端伺服器
- 尋找伊犁鼠兔,巨人網路《球球大作戰》發起野生動物保護公益行動
- 支付寶客戶端架構解析:Android 客戶端啟動速度優化之「垃圾回收」客戶端架構Android優化
- 支付寶客戶端架構解析:iOS 容器化框架初探客戶端架構iOS框架
- Fabric1.4原始碼解析:客戶端建立通道過程原始碼客戶端
- Spring Cloud系列(四):Eureka原始碼解析之客戶端SpringCloud原始碼客戶端
- Java UDP伺服器和客戶端原始碼 -javarevisitedJavaUDP伺服器客戶端原始碼
- 支付寶客戶端架構解析:Android容器化框架初探客戶端架構Android框架
- 支付寶客戶端架構解析:Android 容器化框架初探客戶端架構Android框架
- 高德客戶端低程式碼系統架構實踐客戶端架構
- HDFS原始碼解析:教你用HDFS客戶端寫資料原始碼客戶端
- Linux 伺服器zabbix原始碼客戶端(agent)安裝Linux伺服器原始碼客戶端
- 高德客戶端及引擎技術架構演進與思考客戶端架構
- 【開源遊戲】Legends-Of-Heroes 基於ET 7.2的雙端C#(.net7 + Unity3d)多人線上英雄聯盟風格的球球大作戰遊戲。遊戲C#Unity3D
- Tars-Java客戶端原始碼分析Java客戶端原始碼
- Telegram原始碼之安卓客戶端配置原始碼安卓客戶端
- Dubbo原始碼解析之客戶端初始化及服務呼叫原始碼客戶端
- oracle 客戶端與伺服器端的關係Oracle客戶端伺服器
- js 客戶端與伺服器端的通訊JS客戶端伺服器
- 微信Android客戶端架構演進之路Android客戶端架構
- Eureka客戶端原始碼解析 註冊/心跳/本地重新整理/下線客戶端原始碼
- MapReduce——客戶端提交任務原始碼分析客戶端原始碼
- Java中OpenAI API客戶端原始碼教程JavaOpenAIAPI客戶端原始碼
- redis伺服器/客戶端安裝與配置Redis伺服器客戶端
- Easyvision中的伺服器與客戶端伺服器客戶端
- 外賣客戶端容器化架構的演進客戶端架構
- 解析RocketMQ的client客戶端MQclient客戶端
- jQuery整體架構原始碼解析jQuery架構原始碼