基于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对象。将这个房间作为预制体。
这里没有选择将房间连带着墙作为预制体,而是把墙单独拎出来,在生成房间后再在各个房间生成相应形状的墙
2.随机房间的生成
基本思路为:
1.首先在原点生成一个房间
2.在上下左右四个方向随机选择一个方向,间隔一段距离(xoffset或yoffset)再生成一个房间
3.若在预生成的位置上已存在一个房间,则不生成房间,但是基准点仍移动到这个房间上
4.持续步骤3、4,直到生成预定数量的房间时结束
如果选择回退算法,即在步骤3中若检测到目标位置已存在房间,则将基准点回退,选择其他三个方向进行生成,则有可能会导致生成房间进入一个死胡同,在四个方向上都存在已生成的房间。解决这个问题相对麻烦一些,而且生成的地图较为“线性”,所以舍弃了这种方法。
利用一个RoomGenerator对象,绑定脚本进行房间的生成。每生成一个房间,就将其放入一个列表,方便后续的操作。
// Start is called before the first frame updatevoid 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.门与墙的生成
在房间生成之后,进行门的生成。原则是:相邻的房间之间必有门。
给基础房间的预制体绑定脚本,规定如下参数,上面四个代表四个方向的门对象,下面四个表示是否存在门。(门对象的显示与隐藏就要根据后面四个参数确定)
上一部分代码中,对每个房间进行了如下操作,简单来说就是:
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对象,包含碰撞体组件和刚体组件,用于检测玩家是否进入当前房间,从而对小地图的显示进行调整。
将各个形状的围墙作为预制体,绑定给RoomGenerator以便其调用生成。
4.小地图
小地图的基本原理就是:将一个摄像机放在一个能看到全图的高度,然后将画面传到屏幕的小地图显示区域。如果想看到简略版的地图,可以在摄像机上选择只映射某些图层。具体操作在这里不作啰嗦。
一个小技巧:若想要小地图的中心始终显示当前所在房间,只要将小地图摄像机放到MainCamera之下即可
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 framevoid 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游戏。
2.人物动画
目前先制作站立Idle与移动Run两个动作,通过一个参数speed进行切换
动画切换的相关设置如下,像素游戏不需要过渡动画
对于动画参数,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 updatevoid Start(){//开始时获取自身的刚体组件与动画机组件rb = GetComponent<Rigidbody2D>();anim = GetComponent<Animator>();}// Update is called once per framevoid 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进行切换。
参数的作用为:当按下攻击按键,参数变为true;当参数为true时,表示正在进行攻击动作;在攻击动作结束之后,调用EndAttack函数,将参数改为true
小技巧:为了保证攻击的连贯性,我们可以在动作还未播放完时就调用一个函数代表攻击结束,可以接受新的攻击命令,但是当前攻击动画仍在继续播放(后面会优化)。
参考B站视频:使用Unity实现动作游戏的打击感
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.近战攻击判定
基本方法为:
在人物前部放置一个攻击判定物(如下图白圆),在人物进行攻击动作时激活此物体,如果怪物碰撞到此物体则怪物受到攻击,在攻击动作结束时再关闭此物体。
同样地,怪物的普通近战攻击也是如此。
*动画事件的使用
在一段动画中,我们可以插入事件,在动画进行到某一帧时调用事件对应的函数,如下图,在攻击动作的结束帧调用EndAttack函数,隐藏用于攻击判定的白色圆圈。事件调用函数只需要在人物脚本中以public的形式给出即可。
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 );}}}
小技巧:为了增强怪物的受击反馈,我们可以给怪物添加上受击的特效,绑定在怪物身上当受击时显示并开始播放;此外,还可以添加攻击停顿效果,使用协程实现:
五、添加音效
目前的思路是使用一个SoundManager对象用来集成各种音乐的播放。
创建一个空对象,取名为SoundManager,然后添加一个AudioSource组件,绑定一个脚本。
在脚本中,我们要获取到自身对象的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 updatepublic 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引擎的2D像素风Roguelike地下城游戏Demo相关推荐
- 基于Unity引擎的2D像素风Roguelike地下城游戏模块之————背包系统
文章目录 前言 一.基本界面与逻辑 二.数据管理 三.捡拾物品 四.拖拽功能 1.一些准备与修改 2.拖拽的实现(DragHandler接口) 3.背包界面的拖拽 4.数据的完善 五.一些优化 总结 ...
- 基于Unity的2D像素风闯关游戏Demo——SunnyLand
文章目录 前言 一.制作画面截图 二.游戏实机画面 总结 前言 此Demo根据B站up@M_Studio的教程制作 一.制作画面截图 二.游戏实机画面 总结 这个Demo主要用于熟悉Unity引擎与运 ...
- 基于Unity引擎利用OpenCV和MediaPipe的面部表情和人体运动捕捉系统
基于Unity引擎利用OpenCV和MediaPipe的面部表情和人体运动捕捉系统 前言 项目概述 项目实现效果 2D面部表情实时捕捉 3D人体动作实时捕捉 补充 引用 前言 之前做的一个项目--使用 ...
- Unity 2021创建2D休闲点击器游戏视频教程
Unity 2021创建2D休闲点击器游戏视频教程 Learn how to create a 2D Idle Clicker Game in Unity 2021 了解如何在Unity 2021中创 ...
- 像素风放置类游戏-突袭盛宴Raid Party
欢迎各位新老朋友,我们细说P2E(Play to Earn),为广大用户提供一个深入了解链游的平台.不定期的跟大家分享一些精品项目和最新链游方向,也欢迎大家关注我们.我们第二十三期分享的是像素风放置类 ...
- 我们来用Unity做个2D像素boss战
从个人角度出发,<死亡细胞>有很多让我爱不释手的特征:优秀的操作手感,碎片化的剧情,变化多端的随机地图,多种特点明显的敌人,丰富的装备(技能)系统--以及精彩炫酷的Boss战.无论是守护者 ...
- 从零开始制作基于Unity引擎的宝石消消乐(三)
完整项目我已经放到GitHub啦~ GitHub: https://github.com/lucaschen1993/Lukastar 前言 前两篇把消消乐的设计以及基本操作的方法都讲了,这章开始讲消 ...
- 从零开始制作基于Unity引擎的宝石消消乐(一)
完整项目我已经放到GitHub啦~ GitHub: https://github.com/lucaschen1993/Lukastar 市场上有些消消乐真好玩,比如hxxxxxpop,pxxxxsag ...
- 从零开始制作基于Unity引擎的宝石消消乐(二)
完整项目我已经放到GitHub啦~ GitHub: https://github.com/lucaschen1993/Lukastar 前言 上一篇文章说到游戏的设计,现在就来讲一讲游戏内操作方法的实 ...
最新文章
- Linux 操作系统原理 — 网络 I/O 虚拟化
- 为什么「道理都懂,然而执行力差」的现象如此普遍?
- 15家大数据公司被调查,数据行业面临大清洗?
- 后端拼接html能做判断吗,怎么判断是前端bug还是后端bug?
- 手把手教你如何安装Mac OS X 图文详细教程
- java设置按钮调用问题_按钮相关问题:尝试在空对象引用上调用虚方法
- 通过连接池无法连接mysql_连接池无法链接数据库
- 3个框框带你理解EventLoop
- ognl表达式的小知识点
- C# 正则表达式验证数据类型
- 兄弟连新版PHP视频教程(共346讲)
- python django面试题_django面试题(21道)
- linux alsa 音量参数
- SQL SERVER 实用教程(第四版) 实验 1-10 非标准答案
- rfcn 共享_rfcn卷积网络
- OpenStack官方认证(COA)正式回归!
- 读取*.wav音频文件
- DPCRN: Dual-Path Convolution Recurrent Network for Single Channel Speech Enhancement---论文翻译
- C++笔试编程题1:雀魂启动
- CSS水平垂直居中: flex方式