總結:其實也沒有什麼好總結的......英雄遠征這套源碼雖然說體積並不大,麻雀雖小五臟俱全,對於MMORPG網遊的一些基本系統都有完備的實現,雖然實現方法不一定是最好的。除去場景,戰鬥,組隊,任務等那些我文章中有提到的系統,源碼中還有像是幫派,郵件,坐騎,寵物,排行榜相關的模塊,有興趣的同學可以自己去看看。
遊戲中還有幾個比較重要的系統,感覺還是要提一下:
1.心跳包系統:客戶端會以指定時間間隔循壞給服務器發送消息(此處是協議10006),保持和服務器的連接,如果超出了時間,服務器就會關閉和客戶端的連接(看過《三體》的同學應該都知道,就是“搖籃系統”)。遊戲內的心跳包超時時間和超時次數在sd_reader.erl中定義,作爲接收tcp連接的超時參數。
2.定時器系統:源碼中有專門的一個文件夾timer,用於存放需要執行定時任務的代碼。服務器啓動的時候調用timer_frame:start_link()啓動後天定時服務管理進程(是一個gen_fsm),定時檢查需要執行定時任務的模塊。
3.隨機數生成系統:關於erlang僞隨機的說明,可以參考這篇文章:https://www.cnblogs.com/unqiang/p/4180748.html
所以我們需要一個能返回隨機種子的進程。服務器啓動的時候調用mod_rand:start_link()啓動隨機數種子返回進程,當需要生成隨機數的時候,使用該進程返回的種子。
其餘的還有很多細節值得研究,由於精力有限就不深入下去了。
這份代碼到我手上的時候已經不知道經過了多少人的手了,既然是遊戲總會想去玩一下......根據源碼的sql文件配置完數據庫後,遊戲服務器是能運行起來的,但沒有配套的客戶端,對於遊戲中相關功能的實現總少了直觀的理解。
個人用Erlang編寫了一個簡陋的客戶端模擬程序(之前沒發現源碼裏就有模擬客戶端的代碼,發現之後看了下,感覺協議也不太對的上......),可以完成平臺登錄,創建角色,進入遊戲的操作,用來觀察玩家登錄的完整過程足夠了。服務端也做了相應的修改,爲了方便進行程序的調試(主要是添加了shell內方便熱更的方法),兩份代碼打包後上傳百度雲,如果有感興趣的同學對其繼續完善,甚至能還原遊戲內大部分的功能的話,請務必通知我去玩一下。
百度雲地址:鏈接:https://pan.baidu.com/s/1_M4ddOFgACdlxEKpjqDW2w 提取碼:cwjd
客戶端github地址:https://github.com/Hidoshikun/yxyz_client
要運行服務器和客戶端,只需要進入script文件夾,點擊運行server.bat和client.bat即可。要編譯服務端代碼,點擊運行compile.erl即可。
運行服務器和客戶端後,在客戶端上進行模擬平臺登錄,創角,進入遊戲操作的截圖:
客戶端部分說明:
客戶端代碼在test_client/client.erl,包含了平臺登錄,創角,進入遊戲的操作。遊戲源碼自帶的客戶端模擬代碼在src/test/script文件夾裏面。
%%%-------------------------------------------------------------------
%%% @author hidoshi
%%% @copyright (C) 2018, <COMPANY>
%%% @doc
%%% 遊戲客戶端模擬
%%% 模擬過程:平臺登錄 -> 創角 -> 進入遊戲
%%% @end
%%% Created : 17. 十月 2018 10:04
%%%-------------------------------------------------------------------
-module(client).
-author("hidoshi").
-behaviour(gen_server).
-define(IP, "127.0.0.1").
-define(PORT, 5566).
%% API
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
code_change/3]).
-export([start_link/0, login/0, enter_game/0, create_role/0]).
-export([pack/2, write_string/1]).
-record(state, {
player_name = "",
player_id = 0
}).
%% 創角
create_role() ->
gen_server:cast(?MODULE, {create_role}).
%% 平臺登錄
login() ->
gen_server:cast(?MODULE, {login}).
%% 進入遊戲
enter_game() ->
gen_server:cast(?MODULE, {enter_game}).
start_link() ->
{ok, Pid} = gen_server:start(?MODULE, [?IP, ?PORT], []),
case whereis(?MODULE) of
undefined ->
register(?MODULE, Pid);
_ ->
skip
end.
init([Ip, Port]) ->
case gen_tcp:connect(Ip, Port,
[binary, {packet, 0}, {active, false}, {reuseaddr, true}, {nodelay, false}, {delay_send, true}]) of
{error, Reason} ->
io:format("connect server failed reason ~p~n", [Reason]);
{ok, Socket} ->
%% 連接服務器成功
put(socket, Socket),
io:format("connect server successed ~n"),
Pid = spawn(fun() ->client_receive_loop(Socket) end),
gen_tcp:controlling_process(Socket, Pid),
Socket
end,
{ok, #state{}}.
%% 更新進程state
handle_cast({update_state, Key, Value}, State) ->
NewState =
case Key of
player_id ->
State#state{player_id = Value};
player_name ->
State#state{player_name = Value};
_ ->
State
end,
{noreply, NewState};
handle_cast({create_role}, State) ->
%% 隨機名字
BinName = write_string("王" ++ integer_to_list(rand())),
%% 陣營,職業,性別,名字
Data = <<1:8, 1:8, 1:8, BinName/binary>>,
send_msg(pack(10003, Data)),
{noreply, State#state{player_name = BinName}};
handle_cast({login}, State) ->
%% 隨機平臺賬號
Bin1 = write_string("A" ++ integer_to_list(rand())),
Bin2 = write_string("qodqw4dq65s4d"),
%% 用戶ID,時間戳,平臺賬號,ticket,服務器驗證那裏做了屏蔽,這裏除了ID隨便填
Id = State#state.player_id,
Data = <<Id:32, 22222:32, Bin1/binary, Bin2/binary>>,
send_msg(pack(10000, Data)),
{noreply, State};
handle_cast({enter_game}, State) ->
Id = State#state.player_id,
Data = <<Id:32>>,
send_msg(pack(10004, Data)),
{noreply, State};
handle_cast(_Event, _Status) ->
{noreply, _Status}.
handle_call(_Request, _From, State) ->
{reply, noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, State) ->
gen_tcp:close(State),
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%% 循環接收服務器消息
client_receive_loop(Socket) ->
case gen_tcp:recv(Socket, 0) of
{ok, BinData} ->
unpack(BinData),
client_receive_loop(Socket);
{error, Reason} ->
io:format("error happend reason ~p~n", [Reason])
end.
%% 打包字符串
write_string(S) when is_list(S) ->
BinString = iolist_to_binary(S),
Len = byte_size(BinString),
<<Len:16, BinString/binary>>.
%% 打包數據
pack(Cmd, Data) ->
Len = byte_size(Data) + 4,
<<Len:16, Cmd:16, Data/binary>>.
%% 解包服務器返回信息
unpack(Data) ->
<<_Len:16, Cmd:16, Rest/binary>> = Data,
case Cmd of
10000 ->
<<Result:16>> = Rest,
io:format("login result ~p~n", [Result]);
10003 ->
<<Result:16, PlayerId:32>> = Rest,
%% 將創角後獲得的玩家ID寫回進程state
gen_server:cast(?MODULE, {update_state, player_id, PlayerId}),
io:format("create role result ~p, Id ~p~n", [Result, PlayerId]);
10004 ->
<<Result:16>> = Rest,
io:format("enter game result ~p~n", [Result]);
_ ->
io:format("other cmd ~p~n", [Cmd])
end.
%% 用於給服務器發送消息
send_msg(Msg) ->
Socket = get(socket),
case gen_tcp:send(Socket, Msg) of
ok ->
io:format("send msg successed ~n");
{error, Reason} ->
io:format("send msg failed reason ~p~n", [Reason])
end.
%% 生成隨機數
rand() ->
random:seed(erlang:now()),
trunc(random:uniform() * 10000).
爲了配合客戶端的登錄以及方便調試,服務端主要進行了以下修改:
1.安裝mysql,新建數據庫並導入sdzmmo.sql文件,在common.hrl中修改數據庫連接參數
2.添加shell內便捷熱更新方法:在shell內調用u(ModuleName)即可。具體添加的內容:util:mu/1函數,user_defualt.erl模塊
3.修改了心跳包最大超時時間,修改sd_reader.erl中的HEART_TIMEOUT,不至於讓模擬客戶端掛一會就斷開連接掛掉
4.屏蔽了通行證驗證,pp_account:is_bad_pass/4返回true
5.服務器使用了sasl自帶的日誌系統,可在log.config中進行配置,如果需要查看錯誤信息,需要使用rb模塊進行查看