基于Unity引擎的2D像素风Roguelike地下城游戏Demo

文章目录

  • 前言
  • 一、场景搭建
    • 1.基础房间
    • 2.随机房间的生成
    • 3.门与墙的生成
    • 4.小地图
    • 5.摄像机在房间之间的转移
  • 二、人物制作
    • 1.基础设定
    • 2.人物动画
    • 3.基础移动
    • 4.攻击动作
  • 三.战斗系统
    • 1.近战攻击判定
    • *动画事件的使用
    • 2.受伤判定
  • 四、基本怪物制作
  • 五、添加音效
  • 六、基础UI、菜单与结算界面
  • 七、场景切换
  • 八、光照系统
    • 1.来点特效post processing
    • 2.2D光照
  • 总结


前言

这可以算是一个关于unity的基础笔记,用于记录在实现一个简单的roguelike游戏的过程中遇到的问题与解决方法。


一、场景搭建

参考B站up@M_Studio的教程

1.基础房间

这是一个基础的房间,包含一个绘制场景的Tilemap组件、四个门、一个用于标出房间范围的RoomArea对象。将这个房间作为预制体。

这里没有选择将房间连带着墙作为预制体,而是把墙单独拎出来,在生成房间后再在各个房间生成相应形状的墙
基于Unity引擎的2D像素风Roguelike地下城游戏Demo_第1张图片

2.随机房间的生成

基本思路为:
1.首先在原点生成一个房间
2.在上下左右四个方向随机选择一个方向,间隔一段距离(xoffset或yoffset)再生成一个房间
3.若在预生成的位置上已存在一个房间,则不生成房间,但是基准点仍移动到这个房间上
4.持续步骤3、4,直到生成预定数量的房间时结束

如果选择回退算法,即在步骤3中若检测到目标位置已存在房间,则将基准点回退,选择其他三个方向进行生成,则有可能会导致生成房间进入一个死胡同,在四个方向上都存在已生成的房间。解决这个问题相对麻烦一些,而且生成的地图较为“线性”,所以舍弃了这种方法。

利用一个RoomGenerator对象,绑定脚本进行房间的生成。每生成一个房间,就将其放入一个列表,方便后续的操作。

// Start is called before the first frame update
    void Start()
    {
        for (int i = 0; i < roomNumber; i++)
        {
            //生成对象,并将其放入列表
            rooms.Add(Instantiate(roomPrefab, generatorPoint.position, Quaternion.identity).GetComponent<Room>());  //直接获取basicRoom的Room脚本
            
            //改变point位置
            ChangePointPos();
        }

        //上色(开始与结束)
        rooms[0].GetComponent<SpriteRenderer>().color = startColor;
        rooms[roomNumber-1].GetComponent<SpriteRenderer>().color = endColor;

        //显示房间之间的门
        foreach (var item in rooms)
        {
            SetupRoom(item, item.transform.position);
        }
    }

暂时将第一个生成的房间作为起始房间,最后一个房间作为结束房间。

3.门与墙的生成

在房间生成之后,进行门的生成。原则是:相邻的房间之间必有门。
给基础房间的预制体绑定脚本,规定如下参数,上面四个代表四个方向的门对象,下面四个表示是否存在门。(门对象的显示与隐藏就要根据后面四个参数确定)
基于Unity引擎的2D像素风Roguelike地下城游戏Demo_第2张图片
上一部分代码中,对每个房间进行了如下操作,简单来说就是:
1.检测每个房间的四周是否有房间,并对参数作相应调整。
2.根据参数选择是否显示各个方向的门
3.根据参数为各个房间添加相应形状的围墙

//检测周围房间及数量
    public void SetupRoom(Room newRoom,Vector3 roomPosition)
    {
        for (int i = 0; i < rooms.Count; i++)
        {
            for (int j = 0; j < 4; j++) //检测目标房间的四个方向是否存在房间
            {
                if(rooms[i].transform.position.x==roomPosition.x && rooms[i].transform.position.y == roomPosition.y + yOffset)   //在上方
                {
                    newRoom.roomUp = true;
                }else if (rooms[i].transform.position.x == roomPosition.x && rooms[i].transform.position.y == roomPosition.y - yOffset) //下
                {
                    newRoom.roomDown = true;
                }
                else if (rooms[i].transform.position.y == roomPosition.y && rooms[i].transform.position.x == roomPosition.x + xOffset)  //右
                {
                    newRoom.roomRight = true;
                }
                else if (rooms[i].transform.position.y == roomPosition.y && rooms[i].transform.position.x == roomPosition.x - xOffset)       //左
                {
                    newRoom.roomLeft = true;
                }
            }
        }
	······

(代码太长,省略了不重要的部分)

将所有形状的墙都分别做成一个预制体。其中还包括一个WallMap对象,包含碰撞体组件和刚体组件,用于检测玩家是否进入当前房间,从而对小地图的显示进行调整。
基于Unity引擎的2D像素风Roguelike地下城游戏Demo_第3张图片

将各个形状的围墙作为预制体,绑定给RoomGenerator以便其调用生成。
基于Unity引擎的2D像素风Roguelike地下城游戏Demo_第4张图片

4.小地图

小地图的基本原理就是:将一个摄像机放在一个能看到全图的高度,然后将画面传到屏幕的小地图显示区域。如果想看到简略版的地图,可以在摄像机上选择只映射某些图层。具体操作在这里不作啰嗦。

一个小技巧:若想要小地图的中心始终显示当前所在房间,只要将小地图摄像机放到MainCamera之下即可
基于Unity引擎的2D像素风Roguelike地下城游戏Demo_第5张图片

5.摄像机在房间之间的转移

为摄像机添加一个脚本CameraController:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraController : MonoBehaviour
{
    public static CameraController instance;

    public float speed;
    public Transform target;
    private void Awake()
    {
        instance = this;
    }

    // Update is called once per frame
    void Update()
    {
        if (target != null)
        {
            transform.position = Vector3.MoveTowards(transform.position, new Vector3(target.position.x, target.position.y, transform.position.z), speed * Time.deltaTime);     //从一点移动到另一点
        }
    }

    //更新目标
    public void ChangeTarget(Transform newTarget)
    {
        target = newTarget;
    }
}

然后在Room的脚本里加入:

//player进入时,将新房间的坐标给摄像机
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.CompareTag("Player"))
        {
            CameraController.instance.ChangeTarget(transform);
        }
    }

使得Player进入房间时(碰撞到房间的碰撞体时),将新房间的坐标给摄像机,摄像机发现坐标改变则移动到新的房间。

二、人物制作

1.基础设定

暂时先设置这些组件,PlayerController脚本必不可少,需要注意的是,碰撞盒BoxCollider的范围只需要框定在人物的下部,因为这是一个俯视视角的2D游戏。
基于Unity引擎的2D像素风Roguelike地下城游戏Demo_第6张图片

2.人物动画

目前先制作站立Idle与移动Run两个动作,通过一个参数speed进行切换
基于Unity引擎的2D像素风Roguelike地下城游戏Demo_第7张图片
动画切换的相关设置如下,像素游戏不需要过渡动画
基于Unity引擎的2D像素风Roguelike地下城游戏Demo_第8张图片
对于动画参数,Player的脚本里一般这样设置:(这里为了方便下面的叙述,直接展示PlayerController里的全部代码)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    Rigidbody2D rb;
    Animator anim;
    public GameObject littleMap;
    public float speed;

    Vector2 movement;	//用一个二维向量控制人物移动
    // Start is called before the first frame update
    void Start()
    {
    	//开始时获取自身的刚体组件与动画机组件
        rb = GetComponent<Rigidbody2D>();
        anim = GetComponent<Animator>();
    }

    // Update is called once per frame
    void Update()
    {
        movement.x = Input.GetAxisRaw("Horizontal");    //获取水平方向的运动命令
        movement.y = Input.GetAxisRaw("Vertical");      //获取垂直方向的运动命令

        if (movement.x != 0)
        {
            transform.localScale = new Vector3(movement.x,1,1);		//保证人物朝向始终面对水平移动的方向
        }
		
		//保持动画的切换
        SwitchAnim();
	
		//打开与关闭小地图
        if (Input.GetKeyDown(KeyCode.M))
        {
            if (littleMap.activeSelf == false)
            {
                littleMap.SetActive(true);
            }
            else
            {
                littleMap.SetActive(false);
            }
        }
    }

    private void FixedUpdate()
    {
        rb.MovePosition(rb.position + movement * speed * Time.fixedDeltaTime);  //移动:位置=原位置+移动方向单位向量*速度*修正时间

    }

    void SwitchAnim()
    {
        anim.SetFloat("speed", movement.magnitude);     //将速度传给动画控制机
    }
}

可以看到,通过

anim.SetFloat("speed", movement.magnitude)

这个方法可以对动画机的控制参数进行赋值

3.基础移动

通过一个二维向量movement记录移动命令,并通过如下方法保证人物朝向正确。

movement.x = Input.GetAxisRaw("Horizontal");    //获取水平方向的运动命令
movement.y = Input.GetAxisRaw("Vertical");      //获取垂直方向的运动命令
if (movement.x != 0)
{
       transform.localScale = new Vector3(movement.x,1,1);		//保证人物朝向始终面对水平移动的方向
}

利用FixedUpdate进行移动

private void FixedUpdate()
{
        rb.MovePosition(rb.position + movement * speed * Time.fixedDeltaTime);  //移动:位置=原位置+移动方向单位向量*速度*修正时间

}

至于为什么使用FixedUpdate,根据Unity用户手册:

FixedUpdate:调用 FixedUpdate 的频度常常超过 Update。如果帧率很低,可以每帧调用该函数多次;如果帧率很高,可能在帧之间完全不调用该函数。在 FixedUpdate 之后将立即进行所有物理计算和更新。在 FixedUpdate 内应用运动计算时,无需将值乘以 Time.deltaTime。这是因为 FixedUpdate 的调用基于可靠的计时器(独立于帧率)。

Update:每帧调用一次 Update。这是用于帧更新的主要函数。

4.攻击动作

导入攻击动画,并与Idle和Run动作建立转换关系,各自通过一个参数attacking进行切换。
基于Unity引擎的2D像素风Roguelike地下城游戏Demo_第9张图片
参数的作用为:当按下攻击按键,参数变为true;当参数为true时,表示正在进行攻击动作;在攻击动作结束之后,调用EndAttack函数,将参数改为true

小技巧:为了保证攻击的连贯性,我们可以在动作还未播放完时就调用一个函数代表攻击结束,可以接受新的攻击命令,但是当前攻击动画仍在继续播放(后面会优化)。
参考B站视频:使用Unity实现动作游戏的打击感

基于Unity引擎的2D像素风Roguelike地下城游戏Demo_第10张图片

public void EndAttack()
    {
        anim.SetBool("attacking", false);
        anim.SetBool("attacking2", false);
        anim.SetBool("attacking3", false);
    }

若要在攻击的过程中保持面朝方向不改变,可以在改变方向的操作之前检查当前是否处于攻击状态,若是则直接return

if (anim.GetBool("attacking") || anim.GetBool("attacking2") || anim.GetBool("attacking3"))
{
      return;
}
if (movement.x != 0)
{
      transform.localScale = new Vector3(movement.x,1,1);     //保证人物朝向始终面对水平移动的方向
}

利用随机数随机选择攻击动作

//攻击
        if (Input.GetKeyDown(KeyCode.Mouse0))
        {
            randomAttack = Random.Range(0,3);
            switch (randomAttack)
            {
                case 0:
                    anim.SetBool("attacking", true);
                    break;
                case 1:
                    anim.SetBool("attacking2", true);
                    break;
                case 2:
                    anim.SetBool("attacking3", true);
                    break;
            }
            
        }

三.战斗系统

1.近战攻击判定

基本方法为:
在人物前部放置一个攻击判定物(如下图白圆),在人物进行攻击动作时激活此物体,如果怪物碰撞到此物体则怪物受到攻击,在攻击动作结束时再关闭此物体。
基于Unity引擎的2D像素风Roguelike地下城游戏Demo_第11张图片
同样地,怪物的普通近战攻击也是如此。

*动画事件的使用

在一段动画中,我们可以插入事件,在动画进行到某一帧时调用事件对应的函数,如下图,在攻击动作的结束帧调用EndAttack函数,隐藏用于攻击判定的白色圆圈。事件调用函数只需要在人物脚本中以public的形式给出即可。
基于Unity引擎的2D像素风Roguelike地下城游戏Demo_第12张图片

public void EndAttack()
    {
        anim.SetBool("attacking", false);
        anim.SetBool("attacking2", false);
        anim.SetBool("attacking3", false);
        
        isAttacking = false;
    }

2.受伤判定

以人物受伤判定为例。由于人物进行攻击时,往往距离怪物较近且怪物可能也同时在攻击,所以这里在进行攻击动作时不进行受伤判定,也就是攻击时相当于无敌。

//受伤
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.CompareTag("EnemyAttackRange") && !isAttacking)
        {
            if (health > 0)
            {
                anim.SetBool("isHitten", true);
                health--;
            }    
        }
    }

四、基本怪物制作

目前制作的基础怪物包含以下两个子物体,用于移动控制和攻击判定。
在这里插入图片描述
怪物的动画制作与人物动画的制作过程完全一致,这里不再赘述。这里只展示怪物的基础移动与攻击逻辑。

void Update()
    {
        nowTime = Time.time;
        if (nowTime - startTime >= 1)   //每隔一秒改变一次移动方向
        {
            if (Mathf.Sqrt(Mathf.Pow(rb.transform.position.x - birth_x, 2) + Mathf.Pow(rb.transform.position.y - birth_y, 2)) <= activeDistance)
            {
                movement.x = Random.Range(-1f, 1f);
                movement.y = Random.Range(-1f, 1f);
                startTime = nowTime;
            }
            else	//怪物超出活动范围则向出生点移动
            {
                movement.x = birth_x - rb.transform.position.x;
                movement.y = birth_y - rb.transform.position.y;
                startTime = nowTime;
            }
            
        }

        //保证人物朝向始终面对水平移动的方向
        if (movement.x > 0)
        {
            transform.localScale = new Vector3(1, 1, 1);    
        }else if (movement.x < 0)
        {
            transform.localScale = new Vector3(-1, 1, 1);
        }

        //玩家在探测范围内时,追踪玩家
        if(Mathf.Sqrt(Mathf.Pow(rb.transform.position.x - player.transform.position.x, 2) + Mathf.Pow(rb.transform.position.y - player.transform.position.y, 2)) <= detectDistance)
        {
            movement.x = player.transform.position.x - rb.transform.position.x;
            movement.y = player.transform.position.y - rb.transform.position.y;
        }
        //玩家在攻击范围内时,攻击玩家
        if(Mathf.Sqrt(Mathf.Pow(rb.transform.position.x - player.transform.position.x, 2) + Mathf.Pow(rb.transform.position.y - player.transform.position.y, 2)) <= attackDistance)
        {
            attackNowTime = Time.time;
            if (attackNowTime-attackStartTime > 1)
            {
                anim.SetBool("isAttacking", true);
                
                attackStartTime=attackNowTime;
            }
        }
    }

以及怪物的受击反馈:

//怪物受到攻击
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.CompareTag("PlayerAttackRange"))
        {
            health--;
            if (health <= 0)
            {
                anim.SetBool("isDead", true);
            }
            else
            {
                anim.SetBool("isHitten", true);
                isHurt = true;
                movement.x = rb.transform.position.x - player.transform.position.x;
                movement.y = rb.transform.position.y - player.transform.position.y;
                rb.velocity = new Vector2(movement.x , movement.y );
            }
            
        }
    }

小技巧:为了增强怪物的受击反馈,我们可以给怪物添加上受击的特效,绑定在怪物身上当受击时显示并开始播放;此外,还可以添加攻击停顿效果,使用协程实现:

基于Unity引擎的2D像素风Roguelike地下城游戏Demo_第13张图片

五、添加音效

目前的思路是使用一个SoundManager对象用来集成各种音乐的播放。
创建一个空对象,取名为SoundManager,然后添加一个AudioSource组件,绑定一个脚本。
基于Unity引擎的2D像素风Roguelike地下城游戏Demo_第14张图片

在脚本中,我们要获取到自身对象的AudioSource组件,然后获取要播放的AudioClip。再创建一个自身的静态类,利用静态类进行音频的播放。我们可以根据需要播放音频的不同来给audioSource.clip赋不同的值,以达到播放不同音频的效果。

public class SoundManager : MonoBehaviour
{
    public static SoundManager instance;
    public AudioSource audioSource;

    public AudioClip attackAudio1, attackAudio2, hurtAudio, dieAudio;
    private void Awake()
    {
        instance = this;
    }
    // Start is called before the first frame update

    public void playAttackAudio()
    {
        int temp = Random.Range(0, 2);
        if (temp == 0)
        {
            audioSource.clip = attackAudio1;
        }else if (temp == 1)
        {
            audioSource.clip = attackAudio2;
        }
        
        audioSource.Play();
    }
}

六、基础UI、菜单与结算界面

七、场景切换

八、光照系统

1.来点特效post processing

Post Processing视觉效果

2.2D光照

直接看这个吧!反正不需要代码!

总结

至此,包含简单战斗的游戏基础框架已经基本完成,接下来是一些重要的模块,比如:背包系统、物品系统等等。

你可能感兴趣的:(Unity游戏,unity,游戏,游戏引擎)