从0开始的网游ARPG实战案例:暗黑战神(第十章:战斗系统之资源制作与架构设计)

目录

战斗开发分析

场景制作与光照烘焙

配置数据字段更新技巧

客户端请求战斗逻辑

服务器副本处理系统

合法性检验与数据更新

战斗逻辑框架介绍

战斗业务系统

框架代码与地图加载

场景地图初始化

主角人物初始化

动画控制器设置

角色控制界面制作技巧

控制界面初始化

操作数据传递

状态机定义

状态机切换

状态管理器注入逻辑实体

控制器注入逻辑实体

状态输入切换测试


战斗开发分析

ARPG游戏的战斗模式

一般ARPG游戏的战斗模式:

1.请求服务器开始战斗,校验合法则开始加载战斗资源

2.玩家操控角色释放技能

    本质是控制角色播放动作

    对应时间点播放技能特效与音效

3.计算攻击范围与伤害

4.保留相应运算结果,战斗数据发往服务器校验

5.结果合法则发送关卡奖励

ARPG游戏不涉及战斗部分的网络同步。

场景制作与光照烘焙

建立新的场景,拖拽资源包中的副本场景,然后烘焙灯光。

配置数据字段更新技巧

使用新的map.xml,里面新增power端,用来表示去该区域消耗的体力值。

客户端请求战斗逻辑

为了保证战斗消息通信,我们首先在服务器端的GameMsg定义两个消息

    #region 副本战斗相关
    [Serializable]
    public class ReqFBFight
    {
        public int fbid;
    }

    [Serializable]
    public class RspFBFight
    {
        public int power;
        public int fbid;
    }
    #endregion

然后在客户端发送战斗信息

    //点击副本关卡按钮
    public void ClickTaskBtn(int fbid)
    {
        audioSvc.PlayUIAudio(Constants.UIClickBtn);

        //校验体力是否足够
        int power = resSvc.GetMapCfg(fbid).power;
        if(power > pd.power)
        {
            GameRoot.AddTips("体力值不足");
        }
        else
        {
            netSvc.SendMsg(new GameMsg
            {
                cmd = (int)CMD.ReqFBFight,
                reqFBFight = new ReqFBFight
                {
                    fbid = fbid
                }
            });
        }
    }

服务器副本处理系统

    public void ReqFBFight(MsgPack pack)
    {
        ReqFBFight data = pack.msg.reqFBFight;
        GameMsg msg = new GameMsg
        {
            cmd = (int)CMD.RspFBFight,
        };

        PlayerData pd = cacheSvc.GetPlayerDataBySession(pack.session);//用户信息
        int power = cfgSvc.GetMapCfg(data.fbid).power;//所需体力

    }

合法性检验与数据更新

完善服务器副本处理系统和添加客户端接收消息处理

    public void ReqFBFight(MsgPack pack)
    {
        ReqFBFight data = pack.msg.reqFBFight;
        GameMsg msg = new GameMsg
        {
            cmd = (int)CMD.RspFBFight,
        };

        PlayerData pd = cacheSvc.GetPlayerDataBySession(pack.session);
        int power = cfgSvc.GetMapCfg(data.fbid).power;

        if(pd.fuben < data.fbid)
        {
            msg.err = (int)ErrorCode.ClientDataError;
        }
        else if(pd.power < power)
        {
            msg.err = (int)ErrorCode.LackPower;
        }
        else
        {
            pd.power -= power;
            if (cacheSvc.UpdatePlayerData(pd.id, pd))
            {
                RspFBFight rspFBFight = new RspFBFight
                {
                    fbid = data.fbid,
                    power = pd.power
                };
                msg.rspFBFight = rspFBFight;
            }
            else
                msg.err = (int)ErrorCode.UpdateDBError;
        }
        pack.session.SendMsg(msg);
    }
    public void RspFBFight(GameMsg msg)
    {
        GameRoot.Instance.SetPlayerDataByFBStart(msg.rspFBFight);
        MainCitySys.Instance.maincityWnd.SetWndState(false);


    }

战斗逻辑框架介绍

战斗业务系统

添加新的战斗业务系统BattleSys

using UnityEngine;

public class BattleSys : SystemRoot
{
    public static BattleSys Instance = null;

    public override void InitSys()
    {
        base.InitSys();

        Instance = this;
        PECommon.Log("Init BattleSys...");
    }

    public void StartBattle(int mapid)
    {
        GameObject go = new GameObject
        {
            name = "BattleRoot"
        };
        go.transform.SetParent(GameRoot.Instance.transform);
        BattleMgr battleMgr = go.AddComponent<BattleMgr>();

        battleMgr.Init(mapid);
    }
}

其中StartBattle方法在处理服务器返回消息时调用

新添加一个战场管理器BattleMgr

using UnityEngine;

public class BattleMgr : MonoBehaviour
{
    public void Init(int mapid)
    {

    }
}

框架代码与地图加载

添加几个管理器

其中BattleMgr负责各部分的初始化

using UnityEngine;

public class BattleMgr : MonoBehaviour
{
    private ResSvc resSvc;

    private StateMgr stateMgr;
    private SkillMgr skillMgr;
    private MapMgr mapMgr;

    public void Init(int mapid)
    {
        resSvc = ResSvc.Instance;

        //初始化各管理器
        stateMgr = gameObject.AddComponent<StateMgr>();
        stateMgr.Init();
        skillMgr = gameObject.AddComponent<SkillMgr>();
        skillMgr.Init();

        //加载战斗场景地图
        MapCfg mapData = resSvc.GetMapCfg(mapid);
        resSvc.AsyncLoadScene(mapData.sceneName, () =>
         {
             //初始化地图数据
         });

    }
}

场景地图初始化

完善之前的代码

             //初始化地图数据
             GameObject map = GameObject.FindGameObjectWithTag("MapRoot");
             mapMgr = map.GetComponent<MapMgr>();
             mapMgr.Init();

             map.transform.localPosition = Vector3.zero;
             map.transform.localScale = Vector3.one;

             Camera.main.transform.position = mapData.mainCamPos;
             Camera.main.transform.localEulerAngles = mapData.mainCamRote;

             LoadPlayer(mapData);
             audioSvc.PlayBGMusic(Constants.BGHuangYe);

主角人物初始化

    private void LoadPlayer(MapCfg mapData)
    {
        GameObject player = resSvc.LoadPrefab(PathDefine.AAssissnBattleyPlayerPrefab);

        player.transform.position = mapData.playerBornPos;
        player.transform.localEulerAngles = mapData.playerBornRote;
        player.transform.localScale = Vector3.one;
    }

动画控制器设置

给角色添加新的Animator Controller

然后添加之前的PlayerController,用来控制角色移动

角色控制界面制作技巧

控制界面初始化

创建一个PlayerCtrlWnd脚本挂载在该窗口上,并进行初始化

using PEProtocol;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public class PlayerCtrlWnd :WindowRoot 
{

    public Image imgTouch;
    public Image imgDirBg;
    public Image imgDirPoint;

    public Text txtLevel;
    public Text txtName;
    public Text txtExpPrg;

    private float pointDis;
    private Vector2 startPos = Vector2.zero;
    private Vector2 defaultPos = Vector2.zero;

    public Transform expPrgTrans;

    protected override void InitWnd()
    {
        base.InitWnd();
        pointDis = Screen.height * 1.0f / Constants.ScreenStandardHeight * Constants.ScreenOPDis;
        defaultPos = imgDirBg.transform.position;
        SetActive(imgDirPoint, false);

        RegisterTouchEvts();

        RefreshUI();
    }

    public void RegisterTouchEvts()
    {
        OnClickDown(imgTouch.gameObject, (PointerEventData evt) =>
        {
            startPos = evt.position;
            SetActive(imgDirPoint);
            imgDirBg.transform.position = evt.position;
        });
        OnClickUp(imgTouch.gameObject, (PointerEventData evt) =>
        {
            imgDirBg.transform.position = defaultPos;
            SetActive(imgDirPoint, false);
            imgDirPoint.transform.localPosition = Vector2.zero;
            //MainCitySys.Instance.SetMoveDir(Vector2.zero);
        });
        OnDrag(imgTouch.gameObject, (PointerEventData evt) =>
        {
            Vector2 dir = evt.position - startPos;
            float len = dir.magnitude;
            if (len > pointDis)
            {
                Vector2 clampDir = Vector2.ClampMagnitude(dir, pointDis);
                imgDirPoint.transform.position = startPos + clampDir;
            }
            else
            {
                imgDirPoint.transform.position = evt.position;
            }
            //MainCitySys.Instance.SetMoveDir(dir.normalized);
        });
    }

    public void RefreshUI()
    {
        PlayerData pd = GameRoot.Instance.PlayerData;

        SetText(txtLevel, pd.lv);
        SetText(txtName, pd.name);

        //expprg
        int expPrgVal = (int)(pd.exp * 1.0f / PECommon.GetExpUpValByLv(pd.lv) * 100);
        SetText(txtExpPrg, expPrgVal + "%");
        int index = expPrgVal / 10;

        GridLayoutGroup grid = expPrgTrans.GetComponent<GridLayoutGroup>();

        float globalRate = 1.0F * Constants.ScreenStandardHeight / Screen.height;
        float screenWidth = Screen.width * globalRate;
        float width = (screenWidth - 180) / 10;

        grid.cellSize = new Vector2(width, 7);

        for (int i = 0; i < expPrgTrans.childCount; i++)
        {
            Image img = expPrgTrans.GetChild(i).GetComponent<Image>();
            if (i < index)
            {
                img.fillAmount = 1;
            }
            else if (i == index)
            {
                img.fillAmount = expPrgVal % 10 * 1.0f / 10;
            }
            else
            {
                img.fillAmount = 0;
            }
        }
    }
}

然后在开始战斗的时候调用显示该窗口

操作数据传递

首先在BattleMgr中实现最原始的玩家移动和释放技能的方法

    public void SetSelfPlayerMoveDir(Vector2 dir)
    {
        //设置玩家移动
        PECommon.Log(dir.ToString());
    }

    public void ReqReleaseSkill(int index)
    {
        switch(index)
        {
            case 0:
                ReleaseNormalAtk();
                break;
            case 1:
                ReleaseSkill1();
                break;
            case 2:
                ReleaseSkill2();
                break;
            case 3:
                ReleaseSkill3();
                break;
        }
    }

    private void ReleaseNormalAtk()
    {
        PECommon.Log("Click Nomr Atk");
    }
    private void ReleaseSkill1()
    {
        PECommon.Log("Click Skill1");
    }
    private void ReleaseSkill2()
    {
        PECommon.Log("Click Skill2");
    }
    private void ReleaseSkill3()
    {
        PECommon.Log("Click Skill3");
    }

然后在BattleSys中封装一下

最后在PlayerCtrlWnd上通过BattleSys进行调用

    public void ClickNormalAtk()
    {
        BattleSys.Instance.ReqReleaseSkill(0);
    }
    public void ClickSkill1Atk()
    {
        BattleSys.Instance.ReqReleaseSkill(1);
    }
    public void ClickSkill2Atk()
    {
        BattleSys.Instance.ReqReleaseSkill(2);
    }
    public void ClickSkill3Atk()
    {
        BattleSys.Instance.ReqReleaseSkill(3);
    }

状态机定义

通过BattleMgr上的StateMgr来管理各种各样的状态,比如移动和待机。  它们都继承一个IState接口

public interface Istate 
{
    void Enter(EntityBase entity);

    void Process(EntityBase entity);

    void Exit(EntityBase entity);
}
public class StateIdle : Istate
{
    public void Enter(EntityBase entity)
    {
        
    }

    public void Exit(EntityBase entity)
    {
        
    }

    public void Process(EntityBase entity)
    {
        
    }
}
public class StateMove : Istate
{
    public void Enter(EntityBase entity)
    {

    }

    public void Exit(EntityBase entity)
    {

    }

    public void Process(EntityBase entity)
    {

    }
}

状态机切换

在StateMgr中添加状态装换的方法

    private Dictionary<AniState, Istate> fsm = new Dictionary<AniState, Istate>();

    public void Init()
    {
        //初始化状态
        fsm.Add(AniState.Idle, new StateIdle());
        fsm.Add(AniState.Move, new StateMove());
        PECommon.Log("Init StateMgr Done.");
    }

    public void ChangeStatus(EntityBase entity,AniState targetState)
    {
        if (entity.currentAniState == targetState)
            return;

        if(fsm.ContainsKey(targetState))
        {
            fsm[entity.currentAniState].Exit(entity);
            fsm[targetState].Enter(entity);
            fsm[targetState].Process(entity);
        }
    }

状态管理器注入逻辑实体

逻辑实体类也需要StateMgr用来改变状态

public class EntityBase
{
    public AniState currentAniState = AniState.None;

    public StateMgr stateMgr = null;

    public void Move()
    {
        stateMgr.ChangeStatus(this, AniState.Move);
    }

    public void Idle()
    {
        stateMgr.ChangeStatus(this, AniState.Idle);
    }
}

我们在BattleSys加载人物时来让逻辑实体赋值状态管理

控制器注入逻辑实体

逻辑实体基类新增Controller属性

public Controller controller = null;

我们在BattleSys加载人物时来让逻辑实体赋值控制器

        PlayerController playerCtrl = player.GetComponent<PlayerController>();
        playerCtrl.Init();
        entitySelfPlayer.controller = playerCtrl;

状态输入切换测试

BattleMgr中修改下列代码进行测试

 

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值