場景和副本是玩家活動的區域,場景在服務器啓動的時候由mod_scene進程創建,場景信息保存在?ETS_SCENE中,並一直存在;副本在玩家請求進入的時候會創建副本服務進程,當玩家離開的時候會撤銷進程。
場景:
場景相關的模塊爲mod_scene.erl和lib_scene.erl,
場景的基本要素:怪物,NPC,mask(包含場景可移動座標信息),它們在場景初始化的時候被加載。
場景初始化過程:
1.服務器啓動的時候,通過mod_scene.erl啓動一個場景管理進程,將所有場景id載入
2.使用load_scene(SceneId)初始化場景:
load_scene(SceneId) ->
S = data_scene:get(SceneId),
case S#ets_scene.type =:= 2 orelse S#ets_scene.type =:= 3 of
true -> %% 副本、幫會的原始場景,就不加載
ok;
false ->
load_npc(S#ets_scene.npc, SceneId),
load_mon(S#ets_scene.mon, SceneId),
ets:insert(?ETS_SCENE, S#ets_scene{id = SceneId, mon=[], npc=[], mask=[]}),
case data_mask:get(SceneId) of
"" -> ?ERR("場景的座標MASK爲空:~w", [SceneId]);
Mask1 -> load_mask(Mask1, 0, 0, SceneId)
end
end.
場景加載過程:
1.獲取對應的配置信息,根據配置信息創建NPC和怪物,將場景插入ETS_SCENE表中。
2.獲取每一個NPC配置信息,插入ETS_NPC表中
3.獲取每一個怪物配置信息,開啓一個怪物進程(gen_fsm),並打開怪物的戰鬥進程,將怪物信息保存到ETS_MON中。
4.獲取場景座標mask配置信息並加載,將可移動的座標寫入ETS_CENEN_POSES中
場景管理進程state:
-record(state, {auto_sid, auto_eid}).
其中auto_sid負責生成並返回每一個副本的唯一Id
九宮格廣播系統:
當玩家狀態發生變化時,需要將玩家狀態的改變廣播給場景中的其他玩家,(例如玩家進入新場景,需要告知舊場景的人玩家已離開,告知新場景的人玩家出現),而一個完整的場景可能有很多玩家。
爲了減少不必要的廣播,遊戲設計了九宮格系統,將廣播區域範圍限定在以玩家爲中心的九個格子當中。
使用lib_send:send_to_area_scene/4,獲取玩家當前座標九宮格和九宮格內其餘玩家,並向他們廣播消息
場景對應操作:
1.玩家在場景內走路:走路協議12001,修改玩家#player_status裏的x和y座標,將玩家的新座標通過lib_scene:move_broadcast/9廣播給場景內九宮格玩家
1.獲取場景用戶,場景怪物,場景NPC:使用ets:match/2進行搜索ETS_ONLINE,ETS_MON,ETS_NPC
2.進入/離開場景:leave_scene(Status)和enter_scene(Status),會通知場景九宮格內玩家
3.進入場景條件檢查:check_enter(Status,SceneId) 檢查進入副本的條件(等級,道具)
進入普通場景:enter_normal_scene/2 返回新場景Id,X,Y座標
進入副本場景:檢查玩家副本服務進程是否存在,不存在則創建,enter_normal_scene/2
玩家進入場景的判斷:
%% 進入場景條件檢查
check_enter(Status, Id) ->
case get_data(Id) of
[] ->
{false, 0, 0, 0, <<"場景不存在!">>, 0, []};
Scene ->
case check_requirement(Status, Scene#ets_scene.requirement) of
{false, Reason} -> {false, 0, 0, 0, Reason, 0, []};
{true} ->
case Scene#ets_scene.type of
0 -> %% 普通場景
enter_normal_scene(Id, Scene, Status);
1 -> %% 普通場景
enter_normal_scene(Id, Scene, Status);
2 -> %% 副本場景
case is_pid(Status#player_status.pid_dungeon) andalso is_process_alive(Status#player_status.pid_dungeon) of
true ->
enter_dungeon_scene(Scene, Status); %% 已經有副本服務進程
false -> %% 還沒有副本服務進程
Pid = case is_pid(Status#player_status.pid_team) andalso is_process_alive(Status#player_status.pid_team) of
false -> %% 沒有隊伍,角色進程創建副本服務器
mod_dungeon:start(0, self(), [{Status#player_status.id, Status#player_status.pid}]);
true -> %% 有隊伍,由隊伍進程創建副本服務器
mod_team:create_dungeon(Status#player_status.pid_team, self(), [Id, Status#player_status.id, Status#player_status.pid])
end,
case is_pid(Pid) of
false ->
{false, 0, 0, 0, <<"你不是隊長不能創建副本!">>, 0, []};
true ->
enter_dungeon_scene(Scene, Status#player_status{pid_dungeon = Pid})
end
end
end
end
end.
判斷是副本場景還是普通場景,普通場景直接進入,副本場景檢查玩家是否擁有副本服務管理進程,將進入副本的操作發送到進程內繼續執行
副本:
副本相關模塊爲mod_dungeon.erl
每一個副本由一個副本進程創建並維護,當玩家請求進入副本的時候,副本服務進程纔會被創建
副本服務進程的state:
-record(state, {
team_pid = 0, %% 副本內隊伍pid
rl = [], %% 副本服務器所屬玩家
dsrl = [], %% 副本場景激活條件 [[SceneId, IsOpen::boolean, Request::Atom, NPCId, RequestNum, FinishNum]]
dsl =[] %% 副本服務器所擁有的場景 [{SceneId, IsOpen::boolean, Tips::string}]
}).
副本基本要素:副本場景激活條件,副本場景,在副本配置中可取,當達成一定的條件後可激活副本的新場景
通過mod_dungeon:start/3創建副本進程,在lib_scene:check_enter/2中,或者mod_team:create_dungeon/3中調用
start(TeamPid, From, RoleList) ->
{ok, Pid} = gen_server:start(?MODULE, [TeamPid, RoleList], []),
[clear(role, Id) || {Id, _} <- RoleList],
[mod_player:set_dungeon(Rpid, Pid) || {_, Rpid} <- RoleList, Rpid =/= From],
Pid.
創建完副本進程後,需要修改玩家#player_status.pid_dungeon爲當前副本進程pid
一些常用副本操作:
mod_dungeon:join/2,玩家主動加入副本,清除玩家原來副本,副本玩家列表加入新玩家。用於在組隊中非隊長的玩家加入副本。
mod_dungeon:quit/2,out/2,將玩家踢出副本,調用send_out/1,獲取 副本外場景,修改玩家#player_status的座標,場景Pid,將玩家Id從副本進程玩家id列表中刪去
mod_dungeon:clear/2,清除副本進程,調用mod_scene:clear_scene/1,清除場景內容(怪物,NPC,ETS記錄),並停止副本進程。
kill_npc/2,副本殺怪,更新state中的dsrl,檢查如果達到新的場景開放條件,則更新dsl,激活新的副本場景
玩家進入副本的判斷:
handle_call({check_enter, SceneResId}, _From, State) -> %% 這裏的SceneId是數據庫的裏的場景id,不是唯一id
case lists:keyfind(SceneResId, 4, State#state.dsl) of
false ->
{reply, {false, <<"沒有這個副本場景">>}, State}; %%沒有這個副本場景
DS ->
case DS#ds.enable of
false ->
{reply, {false, DS#ds.tip}, State}; %%還沒被激活
true ->
{SceneId, NewState} =
case DS#ds.id =/= 0 of
true -> {DS#ds.id, State}; %%場景已經加載過
false -> create_scene(SceneResId, State)
end,
{reply, {true, SceneId}, NewState}
end
end;
0.根據#ets_scene.type判斷是否副本場景。
1.檢查玩家是否有副本服務進程pid_dungeon,如果沒有則通過mod_dungeon:start/3創建。
2.檢查該副本服務進程場景列表裏是否有該副本場景,沒有則返回失敗。
3.副本服務進程場景列表有該場景,判斷場景是否激活。
4.檢查場景是否加載過,沒加載過的話調用mod_scene:copy_scene/2複製一個副本場景,取代場景列表中原來的場景。此時場景的“唯一ID”代替了場景的資源id,同樣被插入到?ETS_SCENE表中
將玩家傳出副本:
send_out(R) when is_record(R, ets_online) ->
case get_dungeon_id(lib_scene:get_res_id(R#ets_online.scene)) of
0 -> scene_not_exist; %% 不在副本場景
Did -> %% 將傳送出副本
DD = data_dungeon:get(Did),
[Sid, X, Y] = DD#dungeon.out,
Player = gen_server:call(R#ets_online.pid, 'PLAYER'),
lib_scene:leave_scene(Player),
Player1 = Player#player_status{pid_dungeon = none, scene = Sid, x = X, y = Y},
gen_server:cast(R#ets_online.pid, {'SET_PLAYER', Player1}),
{ok, BinData} = pt_12:write(12005, [Sid, X, Y, <<>>, Sid]),
lib_send:send_one(Player1#player_status.socket, BinData)
end.
獲取副本外場景Id, 修改#player_status的場景Id和座標並回寫玩家進程,並廣播玩家切換場景的消息