目录
Entity-Component-System (ECS)概念
ECS(Entity Component System)的应用
前言
在这一篇博客中,我将介绍如何使用unity以及Entity-Component-System (ECS)和Model-View-Controller (MVC)概念来制作一个简单的2D游戏——记忆翻牌游戏。在这一个过程中,我们将会了解游戏循环的基本原理,掌握常见游戏事件的使用及其执行顺序,学会使用unity的简单操作与编程,掌握游戏世界的基本概念。同时还对ECS以及MVC概念有一定的深入了解。
游戏循环基本原理
游戏循环是指游戏引擎在每一帧中执行的一系列步骤,以更新游戏状态并呈现画面。下面是Unity游戏循环的基本原理:
1. 初始化阶段(Initialization Phase):
游戏引擎初始化:创建游戏窗口、图形设备的初始化等。
场景加载:加载游戏场景、资源等。
2. 输入处理阶段(Input Phase):
处理输入:监听玩家的输入,例如键盘、鼠标、触摸屏等。
更新输入状态:根据输入更新游戏中的相应状态,例如控制角色移动。
3. 更新阶段(Update Phase):
更新游戏状态:执行游戏逻辑、物理模拟、碰撞检测等。
更新游戏对象:更新游戏中的各个对象的状态,例如移动、旋转、动画等。
处理游戏事件:触发和处理游戏中的事件,例如触发特定条件下的事件回调。
4. 渲染阶段(Render Phase):
准备渲染:设置渲染目标、清除缓冲区等准备工作。
渲染场景:将游戏对象的可视化表现绘制到屏幕上,包括几何形状、材质、光照等。
后期处理:应用各种后期效果,例如颜色校正、模糊、阴影等。
呈现画面:将渲染结果显示在屏幕上,完成一帧的渲染。
5. 循环重复:
上述步骤会在每一帧中依次执行,形成游戏循环,不断更新游戏状态和渲染画面。
常见游戏事件
当了解了游戏循环的基本原理后,我们可以探讨一些常见的游戏事件以及它们的执行顺序。以下是一些常见的游戏事件及其执行顺序的概述:
1. Start(开始)事件:
在游戏对象被激活或场景加载完成后触发。
用于初始化游戏对象的状态、设置初始数值、获取引用等。
只会在游戏对象的生命周期中执行一次。
2. Update(更新)事件:
在每一帧的更新阶段执行。
用于处理游戏逻辑、输入响应、移动、旋转等实时更新的操作。
执行顺序从场景中的每个激活的游戏对象开始,按照它们在场景中的顺序依次执行。
3. FixedUpdate(固定更新)事件:
在每一帧的物理更新阶段执行。
用于处理物理模拟、碰撞检测等与物理相关的操作。
时间间隔固定,不受帧率的影响,通常每秒执行几次(例如默认每秒执行 50 次)。
4. LateUpdate(延迟更新)事件:
在 Update 事件之后执行。
用于处理在 Update 事件中可能会影响到其他对象的操作。
例如相机跟随、动画更新等需要在其他对象更新完毕后再执行的操作。
5. OnCollisionEnter(碰撞进入)事件:
在游戏对象与其他对象发生碰撞时触发。
用于处理碰撞事件的逻辑,例如触发游戏效果、播放音效等。
6. OnTriggerEnter(触发器进入)事件:
在游戏对象进入触发器时触发。
用于处理触发器事件的逻辑,例如触发任务、切换场景等。
7. OnGUI(绘制GUI)事件:
在渲染阶段之后执行。
用于绘制游戏界面、UI 元素等。
需要注意的是,不同的游戏事件在执行顺序上可能会有差异,具体取决于事件的类型和注册方式。例如,MonoBehaviour 中的事件会按照脚本的顺序进行执行,而物理事件则与物理引擎的更新步骤相关联。
即时模式GUI(IMGUI)
即时模式 GUI(IMGUI)是 Unity 引擎提供的一种简单而直接的图形用户界面(GUI)系统。IMGUI 允许你在游戏循环的 OnGUI 事件中直接编写代码来创建和更新用户界面元素。
IMGUI 是基于立即绘制的概念,每当 OnGUI 事件被触发时,GUI 元素将被立即绘制到屏幕上。IMGUI 使用一系列 GUI 函数来创建不同类型的控件,例如按钮、文本框、滑块等。你可以通过调用这些函数来设置控件的属性、处理用户输入和响应事件。
以下是使用 IMGUI 的基本示例:
using UnityEngine;
public class MyGUI : MonoBehaviour
{
private string playerName = "Player";
private int score = 0;
private void OnGUI()
{
// 创建一个标签显示玩家名称和分数
GUI.Label(new Rect(10, 10, 200, 20), "Player Name: " + playerName);
GUI.Label(new Rect(10, 30, 200, 20), "Score: " + score);
// 创建一个按钮,点击后增加分数
if (GUI.Button(new Rect(10, 60, 80, 20), "Add Score"))
{
score += 10;
}
// 创建一个文本框,用于输入玩家名称
playerName = GUI.TextField(new Rect(10, 90, 120, 20), playerName);
}
}
需要注意的是,IMGUI 是一种立即绘制的方式,每一帧都会重新绘制所有的 GUI 元素,这可能会对性能产生一定影响。因此,对于复杂的 GUI 界面或需要频繁更新的情况,Unity 推荐使用新的 UI 系统(例如 Canvas 和 RectTransform)来构建用户界面。
Entity-Component-System (ECS)概念
ECS(Entity Component System)是一种用于构建游戏和应用程序的软件架构模式。它的核心思想是将游戏对象(Entities)拆分为组件(Components)和系统(Systems),以提供更高效、可扩展和可维护的开发方式。
在传统的面向对象编程中,游戏对象通常是由一个包含各种属性和行为的单个类表示。但是,当游戏对象变得复杂时,这种设计模式可能导致类的膨胀和难以管理。ECS 通过分离数据和行为,提供了一种更灵活的开发方式。
以下是 ECS 的三个核心概念:
1. 实体(Entity):
实体代表游戏中的对象,可以是角色、敌人、道具等。
实体本身只是一个标识符或唯一标识(ID),不包含任何数据或行为。
2. 组件(Component):
组件是实体的数据部分,用于描述实体的特性和属性。
组件是纯数据,通常是结构体或类,只包含属性和数据,没有任何行为。
一个实体可以拥有多个组件,不同的组件可以用于描述实体的不同方面,例如位置、渲染、碰撞等。
3. 系统(System):
系统是处理组件的行为部分,用于操作和处理具有特定组件集合的实体。
系统根据组件的数据执行逻辑和操作,例如更新位置、渲染图形、处理碰撞等。
系统是独立的,可以根据需要创建多个系统来处理不同类型的组件。
ECS 的核心思想是通过将实体解耦为组件和系统,使得系统可以高效地处理具有相同组件集合的实体,提高了性能和可扩展性。此外,ECS 还支持数据驱动的设计,使得开发者可以更容易地优化和并行化代码,以适应现代多核处理器的优势。
Model-View-Controller (MVC)概念
Model-View-Controller(MVC)是一种软件设计模式,用于组织和管理应用程序的结构和交互逻辑。它将应用程序划分为三个主要组件:模型(Model)、视图(View)和控制器(Controller)。每个组件具有不同的责任和功能,以实现分离关注点和提高代码的可维护性。
下面是 MVC 模式中各个组件的功能和职责:
1. 模型(Model):
模型代表应用程序的数据和业务逻辑。
它负责管理数据的读取、存储、处理和验证。
模型通常包含数据结构、数据库操作、业务规则和算法等。
2. 视图(View):
视图负责呈现模型中的数据给用户,并接收用户的输入。
它通常是用户界面(UI)的一部分,例如图形界面、网页或命令行界面等。
视图是对模型数据的可视化呈现,但本身不负责处理数据或逻辑。
3. 控制器(Controller):
控制器充当模型和视图之间的中介,负责协调它们之间的交互。
它接收用户的输入或请求,并根据需要更新模型和视图。
控制器处理用户事件、调用适当的模型操作,并更新响应的视图。
MVC 模式的核心思想是分离关注点,使每个组件专注于自己的任务。模型负责数据和业务逻辑,视图负责用户界面的呈现,而控制器负责处理用户输入和协调模型与视图之间的交互。
游戏概述
好啦,在前面各种基础知识的了解下,我们可以开始制作我们的2D小游戏啦。我们现在准备制作的游戏是记忆翻牌游戏,具体玩法是:开始游戏时,所有卡牌是不可以见到数字的,只有当我们点击的时候,卡牌才会翻转过来,即可以看到卡牌上的数字。当我们点击了两张卡牌翻转后,如果这两张卡牌上的数字不相等,那么卡牌会再次翻转过去;如果这两张卡牌上的数字相等,那么这两张卡牌就会消失。一直这样子不断点击,使得卡牌两两消去,直到所有卡牌被消去为止,即游戏结束,可以点击Restart重新开始游戏。
我们先来看一下这一个游戏的实现效果吧!
先来看一下我们的游戏界面:
这是我们的游戏展示链接:
具体的游戏实现
模型部分(Model)
初始化model
//1.模型Model部分
//初始化model
private List<int> numbers;
private int[,] table;
private int line_column=6;
private int[,] Is_Click;
private bool CanClick=true;
numbers:这是一个List,用于存放可能在表格中出现的数字,同时为了实现数字之间的匹配从而实现消去。
table:这是一个2D整数数组,表示表格中每一个位置的数字。
line_column:表示这一个矩阵(表格)的行和列是多少,为了实现数字之间的两两消去,这一个变量应该为偶数。
Is_Click:这是一个2D整数数组,表示表格中的哪一个位置被点击,同时表示哪一个位置上的数字已经被消去。
CanClick:这是一个布尔值的变量,表示当点击的卡牌有两个时,将不能再点击其它卡牌,直到这两张卡牌再次翻转过去。
初始化各种参数以及Table和List
//初始化各种参数以及List和Table
void Init(){
//首先初始化table、List
table=new int[line_column,line_column];
Is_Click=new int[line_column,line_column];
numbers=new List<int>();
//将数字初始化到list中
for(int i=1;i<=line_column*line_column/2;i++){
numbers.Add(i);
numbers.Add(i);
}
//随机洗牌,将列表中的数据随机打乱
System.Random random = new System.Random();
for (int i = 0; i < numbers.Count - 1; i++){
int j = random.Next(i, numbers.Count);
int temp = numbers[i];
numbers[i] = numbers[j];
numbers[j] = temp;
}
//将列表中的数据转换到表格中来
//同时将Is_Click的所有数据初始化为0
for(int i=0;i<line_column;i++){
for(int j=0;j<line_column;j++){
table[i,j]=numbers[i*line_column+j];
Is_Click[i,j]=0;
}
}
}
我们需要初始化table、Is_Click以及numbers,首先赋值给numbers,然后将这一个List里面的数据进行随机打乱,最后再将这一个List的数字映射到table中,同时还需要将Is_Click的所有数据初始化为0。
视图部分(View)
//2.视图View部分
void OnGUI(){
GUI.Box(new Rect(255,50,70*line_column,70*line_column),"");
if(GUI.Button(new Rect(215+line_column/2*70,65+line_column*70,100,30),"Restart")){
Init();
}
for(int i=0;i<line_column;i++){
for(int j=0;j<line_column;j++){
if(Is_Click[i,j]==0&&GUI.Button(new Rect(255+j*70,50+i*70,70,70),"")&&CanClick){
Is_Click[i,j]=1;
StartCoroutine(Is_Fade_Away());
}
else{
if(Is_Click[i,j]==1){
GUI.Button(new Rect(255+j*70,50+i*70,70,70),table[i,j].ToString());
}
}
}
}
if(GameOver()){
GUI.Box(new Rect(260,50,70*line_column,70*line_column),"\n\n\n\n\n\n\n\nCongratulations!\n");
}
}
这是一个OnGUI的主函数,我们首先需要创建一个Box,然后在这一个Box中生成一个Button展示字符串“Restart!”,实现重新开始游戏的功能。随后开始生成line_column*line_column的矩阵,生成line_column*line_column个Button。然后开始进行逻辑判断,当点击该卡牌,同时Is_Click为0时,将这一个Is_Click的值赋为1,然后将所有Is_Click为1的卡牌翻转,展示数字。而当点击了两张卡牌后,就需要进行判断,这两张卡牌是否相等,如果相等,就将Is_Click的值赋为2,如果不相等,就将这两张卡牌所在的位置的Is_Click赋为0。最后,直到所有的卡牌对应的位置的Is_Click的值为2,即所有的卡牌都已经被消去了,那么游戏结束,打印“Congratulations!”。
控制部分(Controller)
判断是否将两张卡牌消去的主要逻辑实现
//3.控制Components/Controller部分
//判断是否将两张卡牌消去的主要逻辑实现
IEnumerator Is_Fade_Away(){
CanClick=false;
List<int> counts=Find_element(1);
if(counts.Count==2){
int i_1=counts[0]/line_column;
int j_1=counts[0]-i_1*line_column;
int i_2=counts[1]/line_column;
int j_2=counts[1]-i_2*line_column;
if(table[i_1,j_1]==table[i_2,j_2]){
yield return new WaitForSeconds(0.5f);
Is_Click[i_1,j_1]=2;
Is_Click[i_2,j_2]=2;
}
else{
yield return new WaitForSeconds(0.5f);
Is_Click[i_1,j_1]=0;
Is_Click[i_2,j_2]=0;
}
}
CanClick=true;
}
判断卡牌是否消去,首先需要获取Is_Click中值为1的元素的位置,然后判断为1的元素有多少个,如果只有1个1,将不做处理,如果有2个1,那么将进行判断这两个位置上的数字是否相等。如果相等,就将Is_Click的值赋为2;如果不相等,就将Is_Click的值重新赋为0。同时注意到,为了限制在只能点击两张卡牌,即只能同时看到两张卡牌的数字,我们在进入到这一个函数的时候将CanClick设为了false,然后在退出这一个函数的时候将CanClick设为了true。同时,为了实现当两张卡牌不等时,为了增加记忆时间,我们将使用协程的方法使得函数在这里等待0.5秒的时间。
查找元素的个数以及返回所在位置
//查找Is_Click里面值为1的元素有多少个,将位置返回
List<int> Find_element(int element){
List<int> counts=new List<int>();
for(int i=0;i<line_column;i++){
for(int j=0;j<line_column;j++){
if(Is_Click[i,j]==element){
counts.Add(i*line_column+j);
}
}
}
return counts;
}
这个函数用于查找在Is_Click中有多少个1以及返回每一个1所在的位置,我们将这些所在位置存在一个List中,并且将这一个List作为返回值返回。
判断游戏是否结束
//判断游戏是否结束
bool GameOver(){
bool temp=true;
for(int i=0;i<line_column;i++){
for(int j=0;j<line_column;j++){
if(Is_Click[i,j]!=2){
temp=false;
break;
}
}
}
return temp;
}
当且仅当Is_Click里面的所有数据都是2时才会结束游戏,就算有一个不为2也会是返回false。
ECS(Entity Component System)的应用
虽然上述代码中没有明确实现ECS模式,但我们可以考虑如何将它引入游戏中。例如,我们可以创建一个Card组件,其中包含卡片的值、状态等信息,然后编写系统来处理卡片的翻转和匹配。这样,我们可以更好地分离游戏逻辑和数据。
结语
以上就是我们使用unity以及结合ECS和MVC概念制作而成的一个2D小游戏,其中MVC的三个部分明确详细,并且相互制约,相互联系。通过这一个小游戏,我们可以更加清晰地学会了ECS以及MVC概念的具体含义以及应用,也对于入门unity有了更加深刻的理解和帮助。那么,就让我们一起动起手来,使用unity制作一个属于你们的游戏吧!
代码汇总
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ConcerationGame : MonoBehaviour
{
//1.模型Model部分
//初始化model
private List<int> numbers;
private int[,] table;
private int line_column=6;
private int[,] Is_Click;
private bool CanClick=true;
// Start is called before the first frame update
void Start()
{
Init();
}
//初始化各种参数以及List和Table
void Init(){
//首先初始化table、List
table=new int[line_column,line_column];
Is_Click=new int[line_column,line_column];
numbers=new List<int>();
//将数字初始化到list中
for(int i=1;i<=line_column*line_column/2;i++){
numbers.Add(i);
numbers.Add(i);
}
//随机洗牌,将列表中的数据随机打乱
System.Random random = new System.Random();
for (int i = 0; i < numbers.Count - 1; i++){
int j = random.Next(i, numbers.Count);
int temp = numbers[i];
numbers[i] = numbers[j];
numbers[j] = temp;
}
//将列表中的数据转换到表格中来
//同时将Is_Click的所有数据初始化为0
for(int i=0;i<line_column;i++){
for(int j=0;j<line_column;j++){
table[i,j]=numbers[i*line_column+j];
Is_Click[i,j]=0;
}
}
}
//2.视图View部分
void OnGUI(){
GUI.Box(new Rect(255,50,70*line_column,70*line_column),"");
if(GUI.Button(new Rect(215+line_column/2*70,65+line_column*70,100,30),"Restart")){
Init();
}
for(int i=0;i<line_column;i++){
for(int j=0;j<line_column;j++){
if(Is_Click[i,j]==0&&GUI.Button(new Rect(255+j*70,50+i*70,70,70),"")&&CanClick){
Is_Click[i,j]=1;
StartCoroutine(Is_Fade_Away());
}
else{
if(Is_Click[i,j]==1){
GUI.Button(new Rect(255+j*70,50+i*70,70,70),table[i,j].ToString());
}
}
}
}
if(GameOver()){
GUI.Box(new Rect(260,50,70*line_column,70*line_column),"\n\n\n\n\n\n\n\nCongratulations!\n");
}
}
//3.控制Components/Controller部分
//判断是否将两张卡牌消去的主要逻辑实现
IEnumerator Is_Fade_Away(){
CanClick=false;
List<int> counts=Find_element(1);
if(counts.Count==2){
int i_1=counts[0]/line_column;
int j_1=counts[0]-i_1*line_column;
int i_2=counts[1]/line_column;
int j_2=counts[1]-i_2*line_column;
if(table[i_1,j_1]==table[i_2,j_2]){
yield return new WaitForSeconds(0.5f);
Is_Click[i_1,j_1]=2;
Is_Click[i_2,j_2]=2;
}
else{
yield return new WaitForSeconds(0.5f);
Is_Click[i_1,j_1]=0;
Is_Click[i_2,j_2]=0;
}
}
CanClick=true;
}
//查找Is_Click里面值为1的元素有多少个,将位置返回
List<int> Find_element(int element){
List<int> counts=new List<int>();
for(int i=0;i<line_column;i++){
for(int j=0;j<line_column;j++){
if(Is_Click[i,j]==element){
counts.Add(i*line_column+j);
}
}
}
return counts;
}
//判断游戏是否结束
bool GameOver(){
bool temp=true;
for(int i=0;i<line_column;i++){
for(int j=0;j<line_column;j++){
if(Is_Click[i,j]!=2){
temp=false;
break;
}
}
}
return temp;
}
}