unity2D备忘志

387 阅读13分钟

一 角色移动

unity里面的transform组件非常好用 transform.right这种枚举值真的很方便 Vector2向量 控制移动方向 Input输入非常非常方便! 后面章节有刚体移动,应用也很广泛
transform.Translate(transform.right*speed*Time.deltaTime);
角色随键盘移动:
    [Range(0,10)]
    public float speed=2;
    private float moveX,moveY;
    private Vector2 moveXY;
    void Update()
    {
        moveX=Input.GetAxisRaw("Horizontal");
        moveY=Input.GetAxisRaw("Vertical");
        moveXY.x=moveX*speed*Time.deltaTime;
        moveXY.y=moveY*speed*Time.deltaTime;
        transform.Translate(moveXY);
    }

二 地图绘制

unity自带的tilemap简直是个神器,因为真的非常简便好用 图片精灵是2D游戏的核心组件,功能非常强大,也很易用
  1. 首先在场景中新建一个Tilemap对象 (瓦片地图)
2. 在资源文件夹中新建一个地图文件夹,在里面新建一个瓦片
3. 将瓦片素材图片进行九宫格裁切
4 设置瓦片
然后选择创建新瓦片层 将目录指定好
这样就完成了一副新的瓦片模板
接下来,为瓦片模板里添加地板,很简单,将切好的九宫图拖进来选好文件夹保存,就建好了地板模板了!(其实单张图也可以导入)
5. 用上面的工具就可以绘制一张简单的地图了
调低tilemap的层级防止遮挡
6. 为tilemap设置碰撞体
选中tilemap节点,为其添加tilemap collider2D组件.
然后到地图资源文件夹下,将想产生碰撞的碰撞类型改为sprite,不想产生碰撞的改为none即可.
这样我们可以看到 水已经是带碰撞体的了
7.碰撞体优化
所有水体每个都有一个碰撞盒子,这样很消耗性能,所以我们可以再给tilemap添加一个composite collider(组合碰撞体)组件.添加此组件会自动为其添加rigidbody2D刚体组件
所以要先取消重力影响,将刚体设为静态
然后在碰撞机中选中 used by composite
这样水体就成为一个碰撞体了.
接下来绘制树木 等环境装饰 这里涉及到了预制体

三. 预制体

灵活使用预制体,可以让工作变得轻松,也更方便管理和修改.
这里最好用预制体的方式
预制体很简单,将场景中的任意物体拖入资源文件夹即可生成
生成后向改变可以进入预制件改动,双击进入预制件改动,改动完退出预制件场景即可
也可以点击任意一个预制件实例,修改完成后 apply all
制作好预制件后将预制件拖入场景中布置前景即可

四 设置角色与前景的层级关系(互相遮挡)

选择 edit→project settings
选择图形选单 , 将相机排序设置为custom Axis (自定义轴) , 再将Y轴设为1 这样就会按照Y轴值计算遮挡并渲染(注意相机排序针对同一层,应将会互相遮挡的物体设置在一层中)
把互相遮挡的元素的锚点设置为支点以获得更好的遮挡效果

五 刚体碰撞

给节点添加刚体组件( rigidbody2D)可以进行物理模拟
1.防止掉落
如果不是垂直2D,应该把gravity设为0,防止物体受重力影响掉落下去.
2.防止碰歪
把Z轴旋转冻结可以防止角色被碰撞体碰歪
3.防止碰撞抖动(刚体移动)
通过刚体移动物体可以防止碰撞抖动
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MyRuby : MonoBehaviour
{
    [Range(0,100)]
    public float speed=2;             //移动速度
    private float moveX,moveY;        //用来存储按键信息
    private Vector2 moveToPosition;   //刚体移动的目标位置向量
    private Rigidbody2D rubyBody;     //先定义一个刚体
    private Vector2 moveVector;       //定义一个移动向量
    private void Start() {
        rubyBody=GetComponent<Rigidbody2D>(); //通过组件查找找到刚体
    }
    void Update()
    {
        moveToPosition=rubyBody.position;     //先将目标位置和刚体位置重合
        moveX=Input.GetAxisRaw("Horizontal"); //水平按键信息 A或←的值为-1  D或→的值为1,不按为0
        moveY=Input.GetAxisRaw("Vertical");   //垂直按键信息 S或↓的值为-1  W或↑的值为1,不按为0
        moveVector.x=moveX;
        moveVector.y=moveY;
        moveToPosition +=moveVector *speed*Time.deltaTime;//再根据按键偏移
        rubyBody.MovePosition(moveToPosition); //最后将目标位置传递给缸体移动函数
    }
}

六 摄像机跟随

window → packagemanager
导入Cinemamachine包
导入完成后,菜单上多了一项cinemachine
添加一个2D相机
在其 虚拟相机的脚本中,为follow选项绑定跟随角色,用Lens→Orthographic Size(正交面积)来设定视野大小
相机遇到边界停留:
添加一个拓展组建 cinemachineConfiner
其需要一个界定其边界的物体
所以我们新建一个空物体(不能是虚拟相机的子物体),并给它添加一个可编辑的2D碰撞机
并编辑到地图边缘
为了防止空节点碰撞角色,将空节点的Is trigger勾选
最后将编辑好的空节点传递给拓展组件
这样就设置好了一个自动跟随相机.

七 角色生命增加

假设角色碰到一颗草莓就可以恢复血量
  1. 设置碰撞机
首先,添加一颗草莓,然后给草莓节点增加一个2D碰撞机
将碰撞机的Is Trigger(触发模式)打开 //出发事件,同时防止角色碰撞到草莓时把它碰到一边去.
2.添加碰撞脚本
Trigger会触发一个叫做OnTriggerEnter2D的函数
我们只需要给草莓加载一个脚本里面有OnTriggerEnter2D函数即可触发
3.other (另一个参与碰撞的物体)
other就是与草莓碰撞的物体,这真是太方便了,我们可以直接得到参与碰撞的另一个物体!!
这里我们做个简单的例子,如果判断是角色碰到了草莓,则角色的HP加一
首先,给角色在角色脚本中添加一个角色的HP属性
    [HideInInspector]   //此句限定 是为了防止HP在unity属性界面中可见
    public int RubyHp;  //公开一个HP的属性
为了方便判定other是角色,给角色加一个tag标签"Player"
这样,草莓脚本中就可以
4. 碰撞逻辑
    private void OnTriggerEnter2D(Collider2D other) {
        if (other.tag=="Player")
        {
            other.GetComponent<MyRuby>().RubyHp++;            
        }
    }
就这么神奇,一句话就做到了!
5. 进一步增加逻辑 //设置生命值上下限,草莓被吃后自毁,保护变量
我们应该在主角脚本中增加角色的最大和最小血量, 同时这些变量应该被保护起来,用方法读取
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MyRuby : MonoBehaviour
{
    private int RubyHp;
    private int RubyMaxHp;
    private int RubyMinHp;
    public int getMaxHp(){return RubyMaxHp;}
    public int getMinHp(){return RubyMaxHp;}
    public int getHp(){return RubyHp;}
    private void Start() {
        RubyMaxHp=5;
        RubyMinHp=0;
    }
    public void changeHp(int change){
        RubyHp=Mathf.Clamp(RubyHp+change,RubyMinHp,RubyMaxHp);//这里用到了一个限制结果上下限的函数
    }
}
然后在草莓脚本中使用这些函数即可
    private void OnTriggerEnter2D(Collider2D other) {
        if (other.tag=="Player")
        {
            if (other.GetComponent<MyRuby>().getHp()<other.GetComponent<MyRuby>().getMaxHp())
            {
                other.GetComponent<MyRuby>().changeHp(1);
                Destroy(this.gameObject);
            }
        }
    }

八. 角色受到伤害

上一节中我们已经做好了血量相关的函数,这让角色受到伤害的设计变得简单
比如我们设计一个角色走上尖刺就伤血的功能
我们添加一个地刺,给他加上2D碰撞机,并将IS trigger选项打开
然后,我们给他写一个脚本制造伤害
1.持续伤害
我们需要的是角色进入该区域后持续受伤,所以我们应该使用另一个触发函数:
OnTriggerStay2D() 看函数名就很好理解. 停留中持续触发
    private void OnTriggerStay2D(Collider2D other) {
        if(other.tag=="Player"){
            other.GetComponent<MyRuby>().changeHp(-1);
        }
    }
好的,这样就能持续触发了,但是,这个函数是每一帧都触发一次,即使是江南狗王孙一峰也会被秒吧
2.设定触发时间
既然OnTriggerStay2D()每帧都触发.那么我们在里面累计deltatime,计算一下触发事件按时触发就可以了
    private void OnTriggerStay2D(Collider2D other) 
    {
        if(other.tag=="Player")
        {
            damageTimeCount+=Time.deltaTime;
            if(damageTimeCount>2)
            {
                other.GetComponent<MyRuby>().changeHp(-1);
                damageTimeCount=0;
            }
        }
    }
3. 隐藏的坑 //自动休眠
我们运行上面代码发现并没有按时触发,为什么呢?
因为这里有个隐藏的坑:time to sleep 原来,unity为了提高性能,项目中会有一个timetosleep的设置,当两个物体的相对运动不满足两个线速度和角速度的下限情况下,为了节约性能就sleep了。
因此我们应该修改这个值:
projectSetting->Physic2dSettings->timetosleep
设为20以后,就没有这个问题了.

九. 平铺图片

刚刚的陷阱,如果我们想让它面积大一点,但不要失真,这就要使用到平铺图片了
我们选中图片,将精灵渲染组件的渲染模式改为tiled
就可以调整图片大小而不失真了

十. 制作敌人

敌人和角色一样都有移动速度,血量,碰撞盒和刚体.这里就不加赘述了
但添加刚体时注意:将刚体类型改为Kinematic(运动的),以防止被主角撞得到处乱跑
敌人于角色不同之处在于AI,其行为不是玩家控制的
  1. 水平/垂直移动
我们做一个最脑残的AI 设置敌人左右移动或上下移动
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class enemy1 : MonoBehaviour
{
    public float speed;                      //速度
    private Rigidbody2D enemy1Rigidbody;     //刚体
    private Vector2 moveTo;                  //移动向量
    public bool verticalMove;                //是否是垂直移动
    // Start is called before the first frame update
    void Start()
    {
        enemy1Rigidbody=GetComponent<Rigidbody2D>();
    }

    // Update is called once per frame
    void Update()
    {
        moveTo=enemy1Rigidbody.position;         //更新向量位置
        if (verticalMove)
        {
            moveTo.y += speed*Time.deltaTime;    //垂直移动
        }
        else
        {
            moveTo.x += speed*Time.deltaTime;    //水平移动
            
        }
        enemy1Rigidbody.MovePosition(moveTo);   //刚体移动
    }
}
2. 来回移动
上面的移动只会向一个方向移动,碰到东西也会往前硬挤.我们加一个循环时间,让他往复运动
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class enemy1 : MonoBehaviour
{
    public float speed;
    private Rigidbody2D enemy1Rigidbody;
    private Vector2 moveTo;
    private Vector2 moveDirection;
    private float moveDirectionChange;                      //往复时间计时器
    private float moveDirectionChangeTime=2;                //往复时间设定
    public bool verticalMove;
    // Start is called before the first frame update
    void Start()
    {
        enemy1Rigidbody=GetComponent<Rigidbody2D>();
        moveDirection=verticalMove?Vector2.up:Vector2.right;
        moveDirectionChangeTime=2;
    }

    // Update is called once per frame
    void Update()
    {
        moveDirectionChange+=Time.deltaTime;                        //很简单的逻辑
        if(moveDirectionChange>moveDirectionChangeTime){
            moveDirection *=-1;
            moveDirectionChange =0;
        }
        moveTo=enemy1Rigidbody.position;
        if (verticalMove)
        {
            moveTo.y += speed*Time.deltaTime*moveDirection.x;
        }
        else
        {
            moveTo.x += speed*Time.deltaTime*moveDirection.y;
            
        }
        enemy1Rigidbody.MovePosition(moveTo);
    }
}
这样一个脑残的AI就做完了
把他做成预制体就可以批量生成了.
3. 碰撞检测
因为两种碰撞体,都是刚体,所以碰撞细节更充分,但不再用触发系函数了,转而用碰撞函数OnCollisionEnter2D();
    private void OnCollisionEnter2D(Collision2D other) {
        if (other.gameObject.tag=="Player")
        {
            other.gameObject.GetComponent<MyRuby>().changeHp(-1);
        }
    }

十一 物品动画

我们尝试为草莓添加动画
1.找到动画编辑器 (ctrl+6)
2.创建动画
首先,创建动画,选好文件夹.创建即可
我们需要给他建一个4帧动画
点击添加属性按钮,添加动画属性
点击想改变的帧,然后点击想改变的属性,添加关键帧
如果对运动规律不满意可以调节曲线
这里我给它调了一个特夸张的动画
预制件的好处来了,直接全部apply,瞬间全屏的果冻小草莓,是不是很心动呢??
十二. 敌人动画 (贴图动画)
这里提一下动画机制: animation工具生产的是动画片段animation clip
其由动画状态机 animator管理,动画状态机animator作为控制器传递给挂在在物体节点上的动画组件Animator
组件
上图中的controller就是动画组件绑定的动画状态机 这里面又涉及了混合树 绑定参数 状态切换等知识点,不是很难,但有些杂,请自行补习.
动画状态机(控制器)
animation负责动画生产

1.找到资源
在图片资源里找到动画素材
2.生成动画
选中四幅动画素材,往场景面板中对应的节点一拖,就会自动生成一个动画,按这种方法生成多个动画
我们可以看到没有walk right 的动画,简单的用walk left加个反转(sprite render里面有个flipX属性),自己做一个就好了
选中 要修改的动画
添加组件flipx即
还有个fixed动画,请自行添加.
3.动画执行时机
首先我们打开动画控制器
在里面新建一个混合树 命名为move
因为是2D运动所以选择2D选项
然后把2D动作直接拖进来
设置动作的触发位置
退出混合树,将已经被混合的单个动作都删除就可以了;剩下的动作中选中move设为默认
给混合树添加参数MoveX和MoveY ;
给混合树绑定这两个参数
然后,我们新建一个参数 fixed,如果游戏中敌人被修理好,就触发fixed动画
给fixed连一个状态变换
编辑状态变换,不需要退出时间,关联好fixed状态
此时我们使用脚本给这个动画传递参数 MoveX, MoveY
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class enemy1 : MonoBehaviour
{
    public float speed;
    private Rigidbody2D enemy1Rigidbody;
    private Animator anim;
    private Vector2 moveTo;
    private Vector2 moveDirection;
    private float moveDirectionChange;
    public float moveDirectionChangeTime=2;
    public bool verticalMove;
    // Start is called before the first frame update
    void Start()
    {
        enemy1Rigidbody=GetComponent<Rigidbody2D>();
        moveDirection=verticalMove?Vector2.up:Vector2.right;
        moveDirectionChangeTime=2;
        anim=GetComponent<Animator>();
    }
    // Update is called once per frame
    void Update()
    {
        moveDirectionChange+=Time.deltaTime;
        if(moveDirectionChange>moveDirectionChangeTime){
            moveDirection *=-1;
            moveDirectionChange = 0;
        }
        moveTo=enemy1Rigidbody.position;
        if (verticalMove)
        {
            moveTo.y += speed*Time.deltaTime*moveDirection.y;
        }
        else
        {
            moveTo.x += speed*Time.deltaTime*moveDirection.x;
        }
        enemy1Rigidbody.MovePosition(moveTo);
        anim.SetFloat("MoveX",moveDirection.x);
        anim.SetFloat("MoveY",moveDirection.y);
    }
    private void OnCollisionEnter2D(Collision2D other) {
        if (other.gameObject.tag=="Player")
        {
            other.gameObject.GetComponent<MyRuby>().changeHp(-1);
        }
    }
}
动画机会根据我们传递的参数选择和播放动画
这里提一下 状态机参数 中的 trigger和bool的区别,trigger就是扣动扳机,扳机扣下后射击,然后扳机自动弹回,下一次发射在扣动扳机即可,bool是状态,如果你设为了true,就会一直为true,直到你flase它.

十三. 角色动画

1.按照上文中的方法制作动画
这里提一句,单帧动画是不能直接拖入节点的(会生成子节点),所以在animation里自行添加
2.给角色添加动画组件,并绑定好控制器
一般上一步会自动帮你创建,没有的话自己创建一下
3.制作动画混合树,绑定数据
角色朝向\移动速度\是否被攻击\是否发射子弹 等信息 应在参数中绑定
各个状态的动画应该制作成混合树,方便管理
做好各中动画之间的过渡条件界定,如:速度大于0转为跑步,被攻击转为hit
注意由hit回idle的过渡设置: //hit被trigger触发后,利用Has Exit Time来回到idle状态
这里,先做了,角色移动的动画,主要思路是通过判定,角色移动,给动画状态机传递角色面朝方向 速度
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MyRuby : MonoBehaviour
{
    [Range(0,100)]
    public float speed;
    private int RubyHp;
    private int RubyMaxHp;
    private int RubyMinHp;
    public int getMaxHp(){return RubyMaxHp;}
    public int getMinHp(){return RubyMaxHp;}
    public int getHp(){return RubyHp;}
    private float moveX,moveY;
    private Vector2 moveToPosition;
    private Vector2 moveVector;
    private Vector2 lookVector;
    private Rigidbody2D rubyBody;
    private Animator anim;
    private void Start() {
        rubyBody=GetComponent<Rigidbody2D>();
        anim=GetComponent<Animator>();
        RubyMaxHp=5;
        RubyMinHp=0;
        RubyHp=5;
        lookVector=new Vector2(0,-1);
    }
    void Update()
    {
        moveToPosition=rubyBody.position;
        moveX=Input.GetAxisRaw("Horizontal");
        moveY=Input.GetAxisRaw("Vertical");
        moveVector.x=moveX;
        moveVector.y=moveY;
        if(moveVector.magnitude!=0){ //判断,移动了才会转脸.
            lookVector=moveVector;
            anim.SetFloat("lookX",lookVector.x);  //lookX,lookY可以控制静止和移动时的朝向动画
            anim.SetFloat("lookY",lookVector.y);
        }
        anim.SetFloat("speed",moveVector.magnitude);  //传递移动向量的长度值,状态机根据此速度判断用哪个动画
        moveToPosition +=moveVector *speed*Time.deltaTime;
        rubyBody.MovePosition(moveToPosition);
        Debug.Log(RubyHp);
    }

    public void changeHp(int change){
        RubyHp=Mathf.Clamp(RubyHp+change,RubyMinHp,RubyMaxHp);
    }
}