unity学习-2Dmobilegame(1)

unity学习总结

学习背景

边上课边学了三个月unity,最近在跟着unity的一个2D手机游戏教程学习,到现在大概学习了65%,第一次制作2D游戏(实际上是第一次制作完整游戏hh),遇到很多bug和困难,很多时候不想完全跟着教程走,走了很多弯路但也收获许多知识,在此做总结并记录未解决的疑惑等待未来解决

知识点

1.关于update与fixedupdate
 fixedupdate默认是每秒50次,但对于现在制作的小游戏来说若不加限制update可以达到几百甚至几千,导致把跳跃、攻击等检测输入放在fixedupdate中出现检测不灵敏的问题,解决方法一是可以把getkeydown等代码放在update中执行(不推荐,结构搞得很乱,到了大型游戏的时候又不合理了),二是限制游戏的最大帧数(手机游戏一般30-50帧)使update与fixedupdate大致匹配
 一般来说与物体运动和动作有关的物理逻辑部分统一应该放在fixedupdate中

    private void Awake()
    {
        Application.targetFrameRate = 30;
    }

edit->project settings->quality->VSync改为不同步不然就会尽cpu的全力去跑帧数
最后可以在game面板中显示state查看效果在这里插入图片描述
2.关于代理delegate
 https://blog.csdn.net/hacning/article/details/118723472
3.基类,抽象类,接口的使用
 抽象类 abstract
 接口 interface (命名为Ixxxxxxx)
先说说抽象类和接口的异同

  • 同:
    • 抽象类和接口中都不能直接写方法的具体内容,只能声明框架结构;
    • 继承的子类中必须完整实现它们规定的方法
  • 异:
    • 一个类可以继承多个接口但只能继承一个类
    • 接口只能继承接口,抽象类可以继承接口和其他类
    • 接口内不支持访问方式的声明(public,private),默认都是public

其实这些都不是最重要的,重要的是为什么要用以及什么时候用
首先一个游戏中有很多东西是共性的,比如玩家会受到伤害,敌人也会,甚至一些环境中的物体,那么受伤这个功能我们就希望只写一次来供所有需要的物体访问,那么共性的东西我们希望只声明一次来统一达到效果,这就是接口的作用

public interface IDamagable
{
    void GetHit(int damage);
}

player中的实现⬇

    public void GetHit(int damage)
    {
        if (anim.GetCurrentAnimatorStateInfo(1).IsName("Hit state"))//不会同时受到多个怪物伤害
        {
            Hp -= damage;
            if (Hp < 1)
            {
                Hp = 0;
                isDead = true;
                Rigidbody.velocity = Vector2.zero;
            }
            anim.SetTrigger("gethit");
        }
    }

在造成伤害的物体(炸弹)脚本中调用接口,也可选择造成不同的伤害对玩家和敌人,框架结构非常清晰↓

if (item.CompareTag("Player")) item.GetComponent<IDamagable>().GetHit(Bombdamage);
if (item.CompareTag("NPC")) item.GetComponent<IDamagable>().GetHit(Bombdamage);

接口的典型例子就是伤害接口(以后遇到了再补充)
对于玩家而言,他的行为是由我们输入控制的,而NPC的行为则是预先设定好的,NPC有时候只是在四处巡逻,有时候要追击玩家,这些对于NPC来说都是不同的状态,所以我们希望通过控制他所处的状态来改变NPC的行为使NPC的相关代码模块化,这就是FSM(有限状态机),它的实现就用到了抽象类来抽象各个状态

public abstract class EnemyBaseState
{
    public abstract void EnterState(Enemy state);//进入状态
    public abstract void OnState(Enemy state);//状态中(update调用)
    public abstract void ExitState(Enemy state);//退出状态
}

抽象类中规定了每个继承的状态类都必须实现类的三个阶段:进入,进行中,退出
不同状态类(巡逻,攻击)↓

public class PatrolState : EnemyBaseState
{
    public override void EnterState(Enemy state)
    {
        state.anim.Play("idle");
        state.speed = 2f;
    }
    public override void OnState(Enemy state)
    {
        if(!state.anim.GetCurrentAnimatorStateInfo(0).IsName("idle"))
        {
            state.animstate = 1;
            state.Move();
        }
        if (Mathf.Abs(state.transform.position.x - state.Target.transform.position.x) < 0.05f)
        {
            state.animstate = 0;
            state.SwitchTarget();
        }
    }
    public override void ExitState(Enemy state)
    {
        
    }
}

巡逻是基本状态所以没有退出的操作
状态转换函数↓(在enemy基类(所有不同种类敌人共同继承的类)中update检测,如果符合条件就调用)

    public void SwitchState(EnemyBaseState nowstate,EnemyBaseState nextstate)
    {
        nowstate.ExitState(this);
        nextstate.EnterState(this);
        currentState = nextstate;
    }
    public void Update()
    {
        currentState.OnState(this);
    }

这样极大的条理化了代码,避免了把一堆代码无脑堆到update中乱七八糟,后面像添加新的状态和新型敌人也方便很多

总结:无论是基类,FSM还是接口,都是在提取游戏中共性的部分以简化代码,增强了代码延展性
4.人物移动的选择
 人物移动的可调用方法可谓是五花八门,有时候不同方法间差距甚小,有时候天差地别,那么该怎么选择呢?
 需要考虑的大概有这么几点:

  • 是否是绝对输入控制的(非专业名称 😐)
private void Movement()
{
   float HorizontalInput = Input.GetAxisRaw("Horizontal");
   Rigidbody.velocity = new Vector2(HorizontalInput * speed, Rigidbody.velocity.y);
   if (HorizontalInput != 0) this.transform.localScale = new Vector3(originalScale * HorizontalInput, transform.localScale.y, transform.localScale.z);
   if (Input.GetButton("Jump") && is_grounded)
   {
       JumpFX.SetActive(true);
       JumpFX.transform.position = this.transform.position + JumpFX_offset;
       Rigidbody.velocity = new Vector2(Rigidbody.velocity.x, jumpforce);
       Rigidbody.gravityScale = 5;
   }
}

这个就是绝对输入控制,在fixedupdate中调用时每次都使用当前输入直接覆盖velocity,与之相对的是通过变化来操作的(velocity+=xxxx或者movetowards或者addForce等渐变过程),
顺便说一下,玩家的移动除非是非常简单的移动不然不要直接改position导致逻辑很乱,movetowards等修改position移动一般用于NPC.
 如果要使用绝对覆盖的话就要提前想好有没有其他因素会使人物移动(比如炸弹爆炸),因为绝对覆盖的话其他一切影响都是无效的,产生的力或速度都会立刻被零输入覆盖掉,有的话就必须能够精确判断状态的开始和结束(比如炸弹击飞)然后设置bool值来使这段时间不执行movement,但这并不容易尤其是对结束的判断.
 绝对输入的好处是在这些小游戏中效果非常理想,说停立刻就停说走就走,而且实现简单相对于需要考虑物理因素来说
 对于要求逼真的情况下是肯定要用addForce或者其他渐变方式的,但要注意这意味着物体就必须要有摩擦了不然不会停下来,3D还好2D摩擦真的很麻烦,2D摩擦很容易造成卡墙问题(和墙摩擦太大,摁住左右移动后就掉不下来),而且考虑运动也要复杂的多了

  • 空中是否能移动以及攻击等(接收输入)
     一般来说空中肯定不能随意移动,但实际上空中不移动你会发现整个游戏操控都不对劲了,即便是要求真实性的游戏也最好是添加一个系数来影响空中移动的效率,而对于2D小游戏来说空中肯定是自由输入移动的

5.关于代码的书写规范
切忌在代码中直接使用数字!!!,因为一旦时间长了或者是别人来看你的代码根本不知道你的条件判断中的大于几是什么意思,尽可能地声明变量用变量名代替数字增强可读性.同时应当尽可能的减少代码中变量的可修改位置,在需要修改的时候能够达到改一处而改全部的效果.
6.巧用virtual完善基类与子类的关系
 关于virtual和override的使用情况很多这里举我最近遇到的两例

  1. 初始化获得组件component
     我们常常把一大堆getcomponent的初始化语句都堆在start中,这并不是最佳选择,而且对于有些组件来说只是个别继承类需要获取,并没有必要在基类中获取占用资源,所以我们考虑定义一个 virtual init方法,把公共需要的组件放进去,特殊组件由继承类来改写,然后在awake事件中调用.
    public virtual void Init()//便于子类重写添加一些仅该子类需要初始化的变量,主要是getcomponent
    {
        anim = GetComponent<Animator>();
    }
    private void Awake()
    {
        Init();
    }

这样即节省资源,又把组件获取和变量赋初值分开,先组件获取(awake)后赋初值(start)合乎逻辑,即便不是在基类中我也觉得这样写要优于堆在一起.

  1. 特殊攻击方式override原攻击方法
     不同敌人可能有不同的攻击方法也有可能没有,所以好的方法是在基类中描述共同的攻击方式,对于有特殊攻击的敌人在子类中重写补充.同样也可以用于特殊的运动方式等问题.
    public virtual void Attack()
    {
        attackState.lastAttack = Time.time;
        if(Target.CompareTag("Player"))
        {
            anim.SetTrigger("attack");
            Target.GetComponent<IDamagable>().GetHit(2);
        }
    }
    public override void Attack()
    {
        base.Attack();
        if(Target.CompareTag("Bomb"))
        {
            anim.SetTrigger("skill");
        }
    }

7.方法与动画更好的结合
 其实观察上面代码就会发现,好多时候攻击中没有攻击代码,爆炸中没有爆炸代码,而只是进行播放动画或是改变动画状态参数.这看起来很间接,使我们不能直接从代码中看清执行的线程先后,甚至有些主次不分的感觉,好像由动画决定了事件发生本身,但实际上这很好的避免了一些烦人的bug(动画和实际效果没有一起执行,或者执行不同步) ,它把本身就该同时产生的现象(动画)和本质原因(变量修改)绑定了,也能让你很容易的判断代码有没有被执行通过观察动画是否正常播放.
 盘点一下哪些函数是通过动画的事件调用实现的

  • 部分敌人的技能攻击(例如使炸弹熄灭,在嘴吹起的一帧调用)
  • 人物跳跃和落地特效的setactive(false)在最后一帧
  • 炸弹爆炸动画,第一帧调用爆炸函数,最后一帧调用destroy销毁炸弹
  • 人物落地动画第一帧,调用激活落地特效函数
    总结:最常用的是播放完动画物体的销毁或改layer以及另一个物体的激活,一些要求动作和效果同步(例如吹灭炸弹,如果不同步可能炸弹先灭了才播放吹的动画)的情况也会用到

值得一提的是,对于只需要修改几个组件参数的情况,直接录制帧动画添加property进行修改也是很好的方法(只能修改组件,比如transform,rigidbody,script等,不能用于setactive或修改layer(通过修改脚本间接修改除外)),例如hitpoint检测命中的collider边界变化

8.动画
 坦白地说动画是游戏制作中非常难啃的一块,而且程序员并不是很想去攻克,但显然这对于一个游戏工程师是必须的,从上一点也可以看出动画和脚本密切的联系.于是大概总结一下学到现在遇到的动画使用方法和困难.

  • getcurrentstateinfo和getcurrentclipinfo
    这是脚本对animator的两个主要访问方法,一般来说脚本中都是访问stateinfo来获取当前播放的动画来作为一些判断条件,clipinfo则是获取某个动画的时间来做延时或者协程,stateinfo获取的是状态名而不是动画片段名,这意味着可以用同一个状态名在不同animator中对应不同的clip而只需要用同一行代码来获取判断(这块说实话有点基类子类那味了),获取的时候要注意状态和片段所在的图层.
  • 多图层动画(动画状态机)
    敌人的几个图层
    多图层把动画片段分为了不同状态(匹配状态抽象类继承的多种状态),除了基态外其他都要设置一个空状态作为上一个状态结束或者该状态的开始(例如图中的new state),让物体不在该状态时图层动画停留在这个空状态上.实际上基态最好也设置一个空状态,因为有时候游戏开始直接默认播放第一个动画会出现游戏刚开始的时候物体什么都显示不出来的bug,而用手动play()来控制则不会.在设置不同图层的时候记得要把覆盖调为100%
  • 脚本直接控制和调参控制
    我们可以直接人为调用play()等方法控制播放,也可以通过设置参数(bool,float等)和动画间的转换条件来控制动画播放.后者是我们主要的选择,前者仅会在动画系统特别简单的小游戏中用到,我认为原因主要是一可以把动画中的参数与脚本中的参数直接对应(),符合逻辑;二缩短了脚本中的代码量(把逻辑和判断放到animator中),这对于大量代码的项目很重要,到处play()逻辑很乱debug很困难.注意参数trigger的复位时间容易造成bug,bool会更保险一些不过要自己复位更繁琐.

9.tag与layer
 在攻击对象检测判断等问题上我发现不知道什么时候该用tag什么时候该用layer,感觉好像差不多于是去了解了一番
直接贴大佬文章,讲的极其详细易懂,有根本原理
10.检测
 游戏中有非常多的东西需要检测,敌人,地面等,那么我们可以适当的创建一些Icon和空物体(仅添加collider等检测所需组件)做为子物体来分担检测工作.可以为这些物体创建专门的check layer来使他们能和需检测的物体产生碰撞.在这里插入图片描述
在这里插入图片描述
而且无需为检测物体另创建脚本,因为oncollisionenter和ontriggerenter等message都是可以检测子物体collider的(collision会发生真实物理碰撞,而检测一般使用trigger不发生真实碰撞,勾选is trigger后不会触发oncollision).使用icon则直接获取icon坐标采用射线或球检测即可

    private void Groundcheck()
    {
        if (Physics2D.OverlapCircle(CheckPoint.position, CheckRadius, groundLayer)) {is_grounded = true; Rigidbody.gravityScale = 3; }
        else { is_grounded = false; }
    }

11.延时
 这部分算是未解决的问题吧,因为本身游戏里的执行步骤就很多,现在又要"打乱"执行顺序,感觉这应该是最困难的部分之一
 在创建敌人警戒动画时用到了协程IEnumerator和startcoroutine来确保将警戒动画播放完后再对警戒标识setactive(false);之前尝试用过invoke函数使敌人巡逻到一个点后停止一段时间再继续活动(后来通过播放一次idle动画后再转变目标点解决).只是照猫画虎的解决了问题,不过对其机制的了解和运用次数还远远不够,希望以后能回来补充这部分内容.

未解决的问题

1.unity的摩擦力机制,尤其是2D,加velocity会有加力的效果吗?(2D人物有摩擦挂墙上问题)
2.延时和协程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值