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