Unity简单2D游戏开发

前言:

近日比较无聊,在b站找了一个up主,跟着他的教程来做游戏——开发一个简单的2D游戏

 

 

 

 

 

 


用 Tilemap 绘制场景

新建一个2D项目,在Unity Asset Store中搜索下载 “Pixel Adventure ”,第一个就是我们需要的

然后我们在window中的package manager中搜索下载一个 Tilemap管理2D

安装完成后,在我们的界面中新建一个Tilemap,并将子物体复制两份,并以此修改Layer为0、1、2,方便放置不同物体在不同层

然后在window中找到2D下的TilePalette面板,新建一个new Palette,并取名“Adventure ”,放在一个 palette目录下

tile:瓷砖、地砖

palette:调色板

 

我们现在来导入资源,我们要先用到这个项目资源包中的 Terrain Sliced (16×16),这个文件,找到他,并且做一些属性修改

Unity中自动给它调成100像素,但我们的素材其实是16像素,所以我们改成16,另外MaxSize为1024就足够用了,应用设定。

然后将这个资源拖入Tile Palette 中。

然后就可以开画了,我们先画background

然后做border(是border:边框,不是board,我听错了,不好意思)

然后做这个footground(这是应该是Foreground)

大致场景先做成这样了


人物的等待和跑动

我们将资源下面有个 pink man目录,将下面的 idle 动画拖入场景中,并指定一个新层(Player),并且按6,6放大:

人物出现!

我们将它放到左边合适的位置,然后我们给人物添加2D刚体和2D碰撞盒,刚体注意冻结z轴避免以后发生突破次元的旋转,此时,启动游戏,人物就会受重力影响下坠。

Unity很强大,它自带了一个 Tilemap Collider 2D来给“tile地板”做碰撞检测,我们选住border和footground来添加这个组件,OK,碰撞检测就没有问题了。

然后我们创建动画,先创建Idle,新建动画状态机,将Idle的一帮子图片拖进去,调整好速度,就大功告成。

同样的道理,我们再创建跑步的动画。

然后我们在动画状态机中创建一个混合树来混合Idle和Run动画,用变量speed来控制两个动画的转变。

动画做好就是加入脚本的事情了

给主角挂载一个Player.cs

public class Player : MonoBehaviour
{
    private int speedID = Animator.StringToHash("speed");

    public float speed = 5;  //移动速度
    private Animator _anim;
    private Rigidbody2D _rigidbody;

    private float x;

    void Start()
    {
        _anim = GetComponent<Animator>();
        _rigidbody = GetComponent<Rigidbody2D>();
    }
    void Update()
    {
        //x的范围是-1到1
        x = Input.GetAxis("Horizontal");
        if (x > 0)
        {  //正向移动
            _rigidbody.transform.eulerAngles = new Vector3(0, 0, 0);
            _anim.SetFloat(speedID, 1);
        }
        if(x<0) {
            //反向移动
            _rigidbody.transform.eulerAngles = new Vector3(0, 180, 0);
            _anim.SetFloat(speedID, 1);
        }
        if (x < 0.001f && x > -0.001f) {
            _anim.SetFloat(speedID, 0);
        }
        Run();
    }
    private void Run() {
        Vector3 movement = new  Vector3(x, 0, 0);
        _rigidbody.transform.position += movement * speed * Time.deltaTime;
    }
}

2D的人物控制多是控制在刚体上,如果是3D的就可以控制在charactor controller上。


跳跃

给人物新建一个脚本——Jump.cs

public class Jump : MonoBehaviour
{
    public float jumpVelocity = 5f;

    private Rigidbody2D _rigid;
    private int _jumpID = Animator.StringToHash("jump");
    private bool _jumpRequest = false;

    // Start is called before the first frame update
    void Start()
    {
        _rigid = GetComponent<Rigidbody2D>();   
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetButtonDown("Jump")) {
            _jumpRequest = true;
        }
    }
    //这里为什么要用fixedUpdate?
    //因为我们是对刚体操作,这会和物理引擎发生强关联
    //Unity的物理引擎是按照时间来更新的,FixedUpdate就是按照时间更新
    //而update是按帧更新,不同机子可能帧数不同,从而跳的高度、速度不同
    private void FixedUpdate()
    {
        if (_jumpRequest) {
            //给刚体上方来一个力,力的方式是不断叠加
            _rigid.AddForce(Vector2.up * jumpVelocity, ForceMode2D.Impulse);
            _jumpRequest = false;
        }
    }
}

此时我们就可以实现基本的跳跃,但是我们会发现一个问题,跳跃的比较“没劲”,就像是飘着一样,所以我们再给人物添加一个完善跳跃的脚本

public class BetterJump : MonoBehaviour
{
    //跳跃下降时的加速度
    public float fallMultiplier = 2.5f;
    //跳跃上升时的加速度
    public float lowJumpMultiplier = 2f;
    private Rigidbody2D _rigid;

    private void Start()
    {
        _rigid = GetComponent<Rigidbody2D>();
    }
    private void FixedUpdate()
    {
        //y方向速度小于0,在下降
        if (_rigid.velocity.y < 0)
        {
            _rigid.gravityScale = fallMultiplier;
        }
        //y方向在上升且没有按下空格(即点了一下空格)
        else if (_rigid.velocity.y > 0 && Input.GetButtonDown("Jump"))
        {
            _rigid.gravityScale = lowJumpMultiplier;
        }
        else {
            _rigid.gravityScale = 1;
        }
    }
}

跳跃效果非常成功,但是我们发现出现了一个意想不到的情况,人物可以被卡在边缘。

这是因为人物挂载的碰撞盒与Tilemap的碰撞盒挂住了,我们可以给人物换成 Capsule Collider 2D

 


跳跃触地检测

为了避免人物可以连跳的情况,我们给人物的脚底加上一个检测盒子

先给人物再挂一个脚本——JumpBox,我们会用这个脚本代替上面的Jump.cs脚本

脚本内容:

public class JumpBox : MonoBehaviour
{
    //跳跃向上给的力(前面的[Range(0, 10)]是方便在Unity界面中用线条调整)
    [Range(0, 10)] public float jumpVelocity = 5;
    //检测层
    public LayerMask msk;
    //检测盒子高度
    public float boxHeight;

    private Vector2 _playerSize;
    private Vector2 _boxSize;

    private bool _jumpRequest = false;
    private bool _grounded=false;

    private Rigidbody2D _rigid;
    private void Start()
    {
        _rigid = GetComponent<Rigidbody2D>();
        _playerSize = GetComponent<SpriteRenderer>().bounds.size;
        _boxSize = new Vector2(_playerSize.x* 0.4f, boxHeight);
    }
    private void Update()
    {
        //按下空格且在地上,则可以跳跃
        if (Input.GetButtonDown("Jump") && _grounded) {
            _jumpRequest = true;
        }
    }
    private void FixedUpdate()
    {
        if (_jumpRequest)
        {
            //给刚体上方来一个力,力的方式是不断叠加
            _rigid.AddForce(Vector2.up * jumpVelocity, ForceMode2D.Impulse);
            _jumpRequest = false;
            //跳跃就离开地面了
            _grounded = false;
        }
        else
        {
            //得到检测盒子的中心位置(任务位置偏下)
            Vector2 boxCenter = (Vector2)transform.position +
                Vector2.down * _playerSize.y * 0.5f;
            //利用射线检测盒子
            if (Physics2D.OverlapBox(boxCenter, _boxSize, 0, msk) != null)
            {
                //检测到地面了
                _grounded = true;
            }
            else
            {
                _grounded = false;
            }
        }
    }
    //这个方法是方便在Scene窗口下生成线框帮助检测
    private void OnDrawGizmos()
    {
        if (_grounded)
        {
            Gizmos.color = Color.red;
        }
        else {
            Gizmos.color = Color.green;
        }
        Vector2 boxCenter = (Vector2)transform.position +
            Vector2.down * _playerSize.y * 0.5f;
        Gizmos.DrawWireCube(boxCenter, _boxSize);
    }
}

Gizmos:小玩意

我们可以测试一下,记得将boxHeight先调成0.5左右试一试,然后给Tilemap上的这几层贴上层名,然后指定给脚本中的msk,最后还有 关了原来那个Jump.cs脚本,否则一下会跳的特别高。

OK,到这一步,连续跳跃的问题就解决了,通过射线盒子检测控制一个变量,根据这个变量考虑该不该响应玩家发起的跳跃请求。

顺便一提,这个OnDrawGizmos方法以前我没接触过,是个很好用的调试方法,学到了。


物品收集

在资源中可以找到Fruits目录,我们将里面的某个水果拖到场景中来,我就拿Apple举例了。

老规矩,新建了一个排序层——Items,然后将这个苹果的Order in Layer 设置成了1,并且调整了缩放为 6,6。

然后将图片拖到合适的位置之后,给它一个 Circle Collider 2D,设置成触发器。

同样的道理,我们再选择Fruts下的Collected来做收集物品之后的效果。我们将第一个Collected拖到场景中,然后赋予新的动画状态机,动画内容是这个Collected的这几帧动画,然后,我们将这个Collected作为Apple的子物体,我给他起名字叫了CollectedEffect。

然后,给苹果挂上代码:

public class FruitItem : MonoBehaviour
{
    private GameObject _collectedEffect;

    private SpriteRenderer _spriteRenderer;
    private CircleCollider2D _circleCollider2D;

    private void Start()
    {
        //得到图片渲染组件
        _spriteRenderer = GetComponent<SpriteRenderer>();
        //圆形触发器
        _circleCollider2D = GetComponent<CircleCollider2D>();
        //得到子物体-消失特效,并且先禁用它
        _collectedEffect = transform.Find("CollectedEffect").gameObject;
        _collectedEffect.SetActive(false);
    }
    private void OnTriggerEnter2D(Collider2D collision)
    {
        //玩家触发
        if (collision.gameObject.tag == "Player") {
            _spriteRenderer.enabled = false;
            _circleCollider2D.enabled = false;

            _collectedEffect.SetActive(true);

            //延时0.2秒后消失
            Destroy(gameObject,0.2f);
        }
    }
}

这一套思路很不错,物体的子物体是物体的退场动画,一般是禁用的,当物体被玩家触发则激活子物体,子物体被唤醒播放动画展示效果,0.2秒后销毁。

 


物品收集的UI界面展示

建立Canvas,右上角是水果图片和一个分数文字,图片会循环播放动画,最后效果如下:

我们新建一个全局控制器——GameController来管理分数更新

代码内容:

public class GameController : MonoBehaviour
{
    public int totalScore=0;
    public Text scoreText;

    public static GameController Instance;

    private void Start()
    {
        Instance = this;
        updateScore();
    }
    public void updateScore() {
        scoreText.text = "分数:"+totalScore.ToString();
    }
}

然后在苹果自身脚本的触发检测中加分并且调用这个updateScore,苹果中的脚本的触发检测:

    private void OnTriggerEnter2D(Collider2D collision)
    {
        //玩家触发
        if (collision.gameObject.tag == "Player") {
            _spriteRenderer.enabled = false;
            _circleCollider2D.enabled = false;

            _collectedEffect.SetActive(true);

            GameController.Instance.totalScore += score;
            GameController.Instance.updateScore();
            //延时0.2秒后消失
            Destroy(gameObject,0.2f);
        }
    }

OK!


添加地面陷阱

在资源的找到Spikes,地面尖刺,这个就是我们的地面陷阱

将陷阱拖到游戏场景中,新加一个排序层——Trap,然后把Tag也设设置成Trap,给物体改名Spike

很物体添加组件:Box Collider 2D,调整大小到和尖刺差不多。

做一个UI界面,是游戏失败后重新开始的界面,并且在GameController脚本中这样写:

    public void showGameOverPanel() {
        gameOverPanel.SetActive(true);
        gameOverPanel.transform.Find("Button").GetComponent<Button>()
            .onClick.AddListener(()=> {
                SceneManager.LoadScene("Level1");
            });
    }

给人物脚本Player.cs添加:

    private void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.tag == "Trap") {
            Destroy(gameObject);
            GameController.Instance.showGameOverPanel();
        }
    }

这样一来

 


添加浮动平台

找到Falling Platforms 目录,将里面的on的第一张图片拖入场景合适的位置,修改排序层为Trap,并改缩放为6,6

然后给它添加刚体2D(冻结z轴),再添加组件Target Joint 2D (目标关节),刚体会让它下落,而Targe Joint可以让他保持住位置

把Layer修改为新建的Trap,把那个人物可调跃图层加上这个Trap。

剩下就是给平台挂上脚本

public class FallingPlatform : MonoBehaviour
{
    public float fallingTime = 3;

    private TargetJoint2D _targetJoint2D;
    private BoxCollider2D _boxCollider2D;

    // Start is called before the first frame update
    void Start()
    {
        _targetJoint2D = GetComponent<TargetJoint2D>();
        _boxCollider2D = GetComponent<BoxCollider2D>();
    }

    private void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.tag == "Player") {
            Invoke("Falling",fallingTime);
        }
    }
    private void Falling() {
        _targetJoint2D.enabled = false;
        _boxCollider2D.isTrigger=false;
    }
}

 


弹力蹦床

在资源中找到Trampoline,将Idle拖入到场景中,并修改排序层为Trap,缩放比例为6、6,Layer层为Trap。

给它添加碰撞盒子并且调整缩放到一定大小。

然后我们给它做动画,一个Idle,一个Jump,做好之后Idle为默认状态,Idle转Jump的方式是一个触发变量jump(Trigger变量),然后我们给弹力床挂新脚本

public class Trampoline : MonoBehaviour
{
    public float jumpForce = 17;

    private Animator _anim;
    private int _JumpID = Animator.StringToHash("jump");

    void Start()
    {
        _anim = GetComponent<Animator>();
    }
    void Update()
    {
        
    }
    private void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.tag == "Player") {
            _anim.SetTrigger(_JumpID);
            collision.gameObject.GetComponent<Rigidbody2D>()
                .AddForce(new Vector2(0, jumpForce), ForceMode2D.Impulse);
        }
    }
}

 


添加滚动电锯

我们找到电锯(saw)的图片,拖到场景中,老规矩——改排序层、改层、改缩放(4、4)。然后我们将它拖到某平台上。

然后我们会遇到问题,该怎么样将电锯被平台部分遮住呢?

我们新建一个排序层foreground,然后将foreground设置为该层即可。

给电锯添加碰撞盒来检测玩家的碰撞,

然后给电锯新建动画,电锯的播放效果。

最后,通过脚本来实现电锯的左右移动:

public class Saw : MonoBehaviour
{
    //电锯移动方向
    public float speed = 2;
    //改变方向的时间
    public float moveTime =3;
    private bool _directionRight = true;
    //计时器
    private float _timer ;

    private void Update()
    {
        if (_directionRight)
        {
            transform.Translate(Vector2.right * speed * Time.deltaTime);
        }
        else {
            transform.Translate(Vector2.left * speed * Time.deltaTime);
        }
        _timer += Time.deltaTime;
        if (_timer > moveTime) {
            _timer = 0;
            //这个取反操作好骚啊!
            _directionRight = !_directionRight;
        }
    }
}

这样就可以实现电锯的动态效果了,下一步就是让人物一碰到电锯就会遭遇不测。

在Player的控制脚本中的碰撞检测部分加入这些语句即可

        if (collision.gameObject.tag == "Saw")
        {
            Destroy(gameObject);
            GameController.Instance.showGameOverPanel();
        }

回到Unity给电锯赋一个新标签——Saw

 

 


起落风扇

在资源中找到Fan,将Off拖入场景中,和前面一样,排序层改为Trap,缩放为6、6,调整到合适的位置

然后加上 Box Collider 2D

然后加上一个组件——Area Effector 2D,这个组件是一个区域的物理效果器

将我们的Box Collider 2D 设置为触发器,并且勾选Used By Effector从而使触发器与Area Effector 2D相关联

然后将触发盒子调整的大一些。

然后我们调整Area Effector 2D

Force Angle 调成 90 表示是想正上方的方向,Force Magnitude 调成 20 表示力的大小

然后运行游戏,我们就可以看到基本的推力效果了。

然后我们添加动画,这个操作相信大家都会,和前几个物体一样。

然后,我们添加粒子系统体现风的感觉。

建立一个粒子系统,注意在Render中调整排序层为Trap,这样才能看见。

最后效果:

 

 

 


添加敌人

重置场景

我们复制目前已经完成的场景,命名level2。

在level2中,我们将除了Main Camera、Grid、player之外的元素全部删除掉。

然后我们将前置场景(Grid=》foreground)删掉,重新创建一个Tilemap来绘制不同的场景,这个Tilemap重命名:foreground,排序层调整为2。

然后,绘制这个Tilemap,并且添加Tilemap碰撞检测器,调整图层为 Ground。

 

添加忍者蛙

在资源中找到Ninja Frog,找一张合适的图片拖入场景。

添加刚体、碰撞盒、修改排序层为Enemy,然后将角色拖拽到合适的位置

然后创建动画ninja_run,将所有的run动画拖入,调整好速度即可。

再创建动画ninja_die,将所有的hit动画拖入,调整好速度即可。

然后在ninja_frog的动画状态机中设置,默认动画是run,然后添加一个触发变量(die),使run动画单向指向die动画,不存在退出时间。

Unity界面上的组件就完成了,接下来,我们来创建脚本——NinjaFrog.cs,并将脚本赋予给Ninja Frog;

写代码之前我们先给Ninja Frog创建三个子物体,三个子物体对应了忍者蛙的检测,位置如图:

头部用来检测玩家是否踩到(踩到了忍者蛙,忍者蛙就要死)

右边两个用来检测是否碰墙需要折返。

public class NinjaFrog : MonoBehaviour
{
    [Range(0, 1)] public float speed = 0.1f;

    public LayerMask layer;

    public Transform headPoint;
    public Transform rightUp;
    public Transform rightDown;

    private bool _collided;

    private Animator _anim;
    private Rigidbody2D _rigidbody;
    private CapsuleCollider2D _capsuleCollider2D;


    private int dieID = Animator.StringToHash("die");

    private void Start()
    {
        _rigidbody = GetComponent<Rigidbody2D>();
        _anim = GetComponent<Animator>();
        _capsuleCollider2D = GetComponent<CapsuleCollider2D>();
    }

    private void FixedUpdate()
    {
        Vector3 movement = new Vector3(speed, _rigidbody.velocity.y, 0);
        transform.position += movement;

        //两个参数是线段的起点和终点
        //射线检测到layer层,则返回真,否则为假
        _collided = Physics2D.Linecast(rightUp.position,rightDown.position,layer);

        if (_collided)
        {
            Debug.DrawLine(rightUp.position, rightDown.position, Color.red);
            transform.localScale = new Vector3(transform.localScale.x * -1,
                transform.localScale.y,
                transform.localScale.z);
            speed *= -1;
        }
        else {
            Debug.DrawLine(rightUp.position, rightDown.position, Color.yellow);
        }
    }
    private void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision!=null) {
            if (collision.gameObject.tag == "Player") {
                float height = collision.contacts[0].point.y - headPoint.position.y;
                //玩家踩到忍者蛙
                if (height > 0) {
                    collision.gameObject.GetComponent<Rigidbody2D>()
                                  .AddForce(Vector3.up * 3,ForceMode2D.Impulse);
                    speed = 0;
                    _anim.SetTrigger(dieID);
                    //取消碰撞检测器
                    _capsuleCollider2D.enabled = false;
                    _rigidbody.bodyType = RigidbodyType2D.Kinematic;
                    Destroy(gameObject, 1);
                }
            }
        }
    }
}

最终效果:

 

 


会下落的石头怪

资源中找到RockHead,找到Idle图片,拖到场景中。

调整成Enemy层,添加一个rigidbody2D。

将刚体组件调整成kinematic,冻结x坐标,冻结z旋转。

kinematic

游戏对象是否遵循运动学物理定律,若激活,该物体不再受物理 引擎驱动,而只能通过变换来操作。适用于模拟运动的平台或 者模拟由铰链关节连接的刚体

先给这个石头怪添加一个碰撞盒,盒子和自身大小位置相同。

再给石头怪添加一个触发盒,范围是石头怪正下方的范围(一直到地面)。

添加一个Idle动画、再添加一个Bottom_hit动画,这两个都可以在资源目录下找到。

然后用动画状态机连接,一个触发型变量isBottomHit来进入bottom_hit动画。

然后就是写脚本了,新建一个脚本RockHeadControl.cs

public class BottomHitControl : MonoBehaviour
{
    private static  readonly int _bottomHitID = Animator.StringToHash("isBottomHit");

    private Rigidbody2D _rigidbody2D;
    private Animator _animator;

    private float _timer;
    private const float RockHeadWaitingTimeSpan = 4f;
    private const float RockHeadMoveBackTimeSpan = 0.3f;

    private bool _isGroundHit = false;
    private Vector3 _originalPosition;
    //一个向上的速度
    private Vector3 _velocity = Vector3.zero;

    private void Start()
    {
        _rigidbody2D = GetComponent<Rigidbody2D>();
        _animator = GetComponent<Animator>();
        _originalPosition = _rigidbody2D.position;
    }
    private void OnTriggerStay2D(Collider2D collision)
    {
        if (collision.CompareTag("Player")) {
            _rigidbody2D.isKinematic = false;
            _rigidbody2D.gravityScale = 2f;
        }
    }
    private void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.CompareTag("Player")) {
            Vector3 playerLocalScale =
                collision.gameObject.transform.localScale;

            collision.gameObject.transform.localScale =
                new Vector3(playerLocalScale.x,
                    playerLocalScale.y*0.1f,
                    playerLocalScale.z);

            Vector3 colliderSize =
                collision.gameObject
                .GetComponent<CapsuleCollider2D>()
                .size;

            collision.gameObject
                .GetComponent<CapsuleCollider2D>()
                .size = new Vector3(
                    colliderSize.x * 0.1f,
                    colliderSize.y * 0.1f);

            Destroy(collision.gameObject, 0.5f);
        }

        //地面的层级Ground是第8层
        if (collision.gameObject.layer == 8) {
            _isGroundHit = true;
            _animator.SetTrigger(_bottomHitID);
        }
    }
    private void FixedUpdate()
    {
        if (_isGroundHit) {
            _timer += Time.fixedDeltaTime;
            if (_timer >= RockHeadWaitingTimeSpan &&
                transform.position != _originalPosition)
            {
                //恢复原始状态
                _rigidbody2D.isKinematic = true;
                _rigidbody2D.gravityScale = 1;

                transform.position = Vector3.SmoothDamp(
                        transform.position,
                        _originalPosition,
                        ref _velocity,
                        RockHeadMoveBackTimeSpan
                    );
            }
            else if(transform.position==_originalPosition) {
                _isGroundHit = false;
                _timer = 0;
            }
        }

    }
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

发表评论