英雄远征Erlang源码分析(13)-总结 附上可执行的服务端和客户端代码

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/s291547/article/details/88935204

总结:其实也没有什么好总结的......英雄远征这套源码虽然说体积并不大,麻雀虽小五脏俱全,对于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模块进行查看

猜你喜欢

转载自blog.csdn.net/s291547/article/details/88935204
今日推荐