英雄遠征Erlang源碼分析(7)-場景與副本

場景和副本是玩家活動的區域,場景在服務器啓動的時候由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和座標並回寫玩家進程,並廣播玩家切換場景的消息

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