文章目录

  • 前言
  • 一、场景搭建
    • 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相关推荐

  1. 基于Unity引擎的2D像素风Roguelike地下城游戏模块之————背包系统

    文章目录 前言 一.基本界面与逻辑 二.数据管理 三.捡拾物品 四.拖拽功能 1.一些准备与修改 2.拖拽的实现(DragHandler接口) 3.背包界面的拖拽 4.数据的完善 五.一些优化 总结 ...

  2. 基于Unity的2D像素风闯关游戏Demo——SunnyLand

    文章目录 前言 一.制作画面截图 二.游戏实机画面 总结 前言 此Demo根据B站up@M_Studio的教程制作 一.制作画面截图 二.游戏实机画面 总结 这个Demo主要用于熟悉Unity引擎与运 ...

  3. 基于Unity引擎利用OpenCV和MediaPipe的面部表情和人体运动捕捉系统

    基于Unity引擎利用OpenCV和MediaPipe的面部表情和人体运动捕捉系统 前言 项目概述 项目实现效果 2D面部表情实时捕捉 3D人体动作实时捕捉 补充 引用 前言 之前做的一个项目--使用 ...

  4. Unity 2021创建2D休闲点击器游戏视频教程

    Unity 2021创建2D休闲点击器游戏视频教程 Learn how to create a 2D Idle Clicker Game in Unity 2021 了解如何在Unity 2021中创 ...

  5. 像素风放置类游戏-突袭盛宴Raid Party

    欢迎各位新老朋友,我们细说P2E(Play to Earn),为广大用户提供一个深入了解链游的平台.不定期的跟大家分享一些精品项目和最新链游方向,也欢迎大家关注我们.我们第二十三期分享的是像素风放置类 ...

  6. 我们来用Unity做个2D像素boss战

    从个人角度出发,<死亡细胞>有很多让我爱不释手的特征:优秀的操作手感,碎片化的剧情,变化多端的随机地图,多种特点明显的敌人,丰富的装备(技能)系统--以及精彩炫酷的Boss战.无论是守护者 ...

  7. 从零开始制作基于Unity引擎的宝石消消乐(三)

    完整项目我已经放到GitHub啦~ GitHub: https://github.com/lucaschen1993/Lukastar 前言 前两篇把消消乐的设计以及基本操作的方法都讲了,这章开始讲消 ...

  8. 从零开始制作基于Unity引擎的宝石消消乐(一)

    完整项目我已经放到GitHub啦~ GitHub: https://github.com/lucaschen1993/Lukastar 市场上有些消消乐真好玩,比如hxxxxxpop,pxxxxsag ...

  9. 从零开始制作基于Unity引擎的宝石消消乐(二)

    完整项目我已经放到GitHub啦~ GitHub: https://github.com/lucaschen1993/Lukastar 前言 上一篇文章说到游戏的设计,现在就来讲一讲游戏内操作方法的实 ...

最新文章

  1. Linux 操作系统原理 — 网络 I/O 虚拟化
  2. 为什么「道理都懂,然而执行力差」的现象如此普遍?
  3. 15家大数据公司被调查,数据行业面临大清洗?
  4. 后端拼接html能做判断吗,怎么判断是前端bug还是后端bug?
  5. 手把手教你如何安装Mac OS X 图文详细教程
  6. java设置按钮调用问题_按钮相关问题:尝试在空对象引用上调用虚方法
  7. 通过连接池无法连接mysql_连接池无法链接数据库
  8. 3个框框带你理解EventLoop
  9. ognl表达式的小知识点
  10. C# 正则表达式验证数据类型
  11. 兄弟连新版PHP视频教程(共346讲)
  12. python django面试题_django面试题(21道)
  13. linux alsa 音量参数
  14. SQL SERVER 实用教程(第四版) 实验 1-10 非标准答案
  15. rfcn 共享_rfcn卷积网络
  16. OpenStack官方认证(COA)正式回归!
  17. 读取*.wav音频文件
  18. DPCRN: Dual-Path Convolution Recurrent Network for Single Channel Speech Enhancement---论文翻译
  19. C++笔试编程题1:雀魂启动
  20. CSS水平垂直居中: flex方式

热门文章

  1. 欢迎使用CSDN-markdown编辑器发顺丰 第三个是德国
  2. 大同5中高考2021年成绩查询,大同中学排名前十名,2021年大同中学排名一览表
  3. 机器学习:Kmeans
  4. vue+three+layui 实现后台登陆页面
  5. 名编辑电子杂志大师教程 | 名编辑设计设置面板
  6. 201910010013(作业一)
  7. scrapyd爬虫一直在运行,无法停止
  8. 惊人现象卸载暴风5后残留病毒
  9. 2020世界500强排行榜出炉
  10. 大统一模型可以用计算机,大统一理论模型