英雄遠征Erlang源碼分析(13)-總結 附上可執行的服務端和客戶端代碼

總結:其實也沒有什麼好總結的......英雄遠征這套源碼雖然說體積並不大,麻雀雖小五臟俱全,對於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模塊進行查看

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章