四、脚本编写
(一)创建“细胞”
首先,让我们来创建生命游戏的基本单位——“细胞”。
在Hierarchy窗口中单击右键>2D Object>Sprites>Square,将其重命名为Tile。
接下来,在Project窗口中新建文件夹Scripts,用来存放游戏脚本。在Scripts文件夹中新建C#脚本,重命名为Tile,将这个脚本挂到刚创建的2D物体Tile上。
再在Project窗口中新建文件夹Prefabs,将挂好脚本的2D物体Tile拖入这个文件夹,存为预制体。删掉Hierarchy窗口中的2D物体Tile。
(二)生成网格背景
首先,在Hierachy窗口中单击右键>Create Empty,创建一个空白的新物体,重命名为Grid Manager,然后在Scripts文件夹中新建脚本GridManager,把这个脚本挂在Grid Manager上。
接下来我们开始写GridManager脚本。我们已经有了构成网格的“细胞”,只需通过嵌套for循环就可以生成整张网格了。
    
    
using System . Collections ; using System . Collections . Generic ; using UnityEngine ; public class GridManager : MonoBehaviour { public int width , height ; [ SerializeField ] private Tile tilePrefab ; [ SerializeField ] private Transform cam ; public void Start ( ) { GenerateGrid ( ) ; } void GenerateGrid ( ) { for ( int x = 0 ; x < width ; x ++ ) { for ( int y = 0 ; y < height ; y ++ ) { var spawnedTile = Instantiate ( tilePrefab , new Vector3 ( x , y ) , Quaternion . identity ) ; spawnedTile . name = $"Tile { x } { y } " ; } } cam . transform . position = new Vector3 ( ( float ) width / 2 - 0.5f , ( float ) height / 2 + 1.8f , - 10 ) ; } }
回到Unity窗口,找到Grid Manager上挂载的GridManager脚本组件,将Main Camera拖入Cam栏位,将Tile预制体拖入Tile Prefab栏位,并设置表格的长(Width)宽(Height)。示例中的Width为60,Height为34,Main Camera的Size为20。这些参数,包括脚本中对摄像机位置的参数设置,都可以根据你的需要来调整,直至获得满意效果。
现在我们有了一张“网格”,但这张网格看上去就像一块白板,没有任何分界线。这是因为所有格子都是相同颜色。为了解决这个问题,我们要让格子的颜色交错,方便后面的互动操作和显示。
在脚本Tile中,我们让每个格子根据一个布尔值isOffset来确定自身的颜色。(这里我使用了三目运算符,如果你还不熟悉它的用法,请参考官方文档:https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/operators/conditional-operator)
    
    
using System . Collections ; using System . Collections . Generic ; using UnityEngine ; public class Tile : MonoBehaviour { [ SerializeField ] private Color baseColor , offsetColor ; [ SerializeField ] private SpriteRenderer tileRenderer ; public void Init ( bool isOffset ) { tileRenderer . color = isOffset ? offsetColor : baseColor ; } }
这个布尔值是由格子所在位置决定的。之前生成网格时,我们用格子的坐标x和y给它们起了名字,所以我们回到GridManager脚本中,在这里给isOffset赋值。
    
    
void GenerateGrid ( ) { for ( int x = 0 ; x < width ; x ++ ) { for ( int y = 0 ; y < height ; y ++ ) { var spawnedTile = Instantiate ( tilePrefab , new Vector3 ( x , y ) , Quaternion . identity ) ; spawnedTile . name = $"Tile { x } { y } " ; var isOffset = ( x % 2 == 0 && y % 2 != 0 ) || ( x % 2 != 0 && y % 2 == 0 ) ; spawnedTile . Init ( isOffset ) ; } } cam . transform . position = new Vector3 ( ( float ) width / 2 - 0.5f , ( float ) height / 2 + 1.8f , - 10 ) ; }
当x和y一奇一偶时,isOffset为true,否则为false。
回到Unity窗口,打开Tile预制体,给Base Color、Offset Color和Tile Renderer赋值。注意:(1)Base Color和Offset Color最好是两个相近的颜色。(2)给Tile Renderer赋值时,把Tile预制体上的Sprite Renderer拖进去即可。
现在运行游戏,应该能出现一张漂亮的网格了!
(三)点击鼠标,点亮格子
生命游戏在开始演化之前,需要玩家设定一个初始图案。这个图案是通过鼠标点击格子“画”出来的。此外,我们还希望鼠标悬停在某个格子但尚未点击时,格子也会点亮,并在鼠标离开格子后不再点亮。这样更方便我们判断鼠标当前所在位置。
为了呈现出点亮格子的效果,我们先打开Tile预制体添加一个子物体:单击右键>2D Object>Sprites>Square,将其重命名为Highlight。然后,将Highlight的颜色Alpha值调为120。
调整之后,取消勾选Highlight前面的对勾,使之隐藏。
接下来打开Tile脚本,输入以下语句:
    
    
public bool isClicked = false ; public GameObject highlight ; void OnMouseEnter ( ) { highlight . SetActive ( true ) ; } void OnMouseExit ( ) { if ( isClicked == false ) { highlight . SetActive ( false ) ; } } void OnMouseDown ( ) { isClicked = ! isClicked ; if ( isClicked == true ) { highlight . SetActive ( true ) ; } else { highlight . SetActive ( false ) ; } }
回到Unity窗口,打开Tile预制体,将Hierarchy窗口中Tile的子物体Highlight拖入Tile脚本组件的Highlight栏位。
现在,运行游戏,点击鼠标,将可以点亮格子,画出你想要的图案了。
(四)获取周边格子的信息
一般来说,网格中任意一格周边会有八个格子(如下图),每个格子周边格子的状态决定了这个格子下一轮的状态,根据这些状态,画布上将出现新的“图案”。
在Tile脚本中添加以下内容:
    
    
public GameObject GridManager ; public int nameX ; public int nameY ; int gridCountX ; int gridCountY ; public List < GameObject > tempCells = new List < GameObject > ( ) ; void Start ( ) { gridCountX = GridManager . GetComponent < GridManager > ( ) . width ; gridCountY = GridManager . GetComponent < GridManager > ( ) . height ; for ( int i = - 1 ; i <= 1 ; i ++ ) { for ( int j = - 1 ; j <= 1 ; j ++ ) { int tempX = nameX + i ; int tempY = nameY + j ; if ( tempX < gridCountX && tempX >= 0 && tempY < gridCountY && tempY >= 0 && ( tempX != nameX | tempY != nameY ) ) { tempCells . Add ( GameObject . Find ( $"Tile { tempX } { tempY } " ) ) ; } } } }
为了获得周边格子的信息,我们创建了一个列表来储存它们。
主体是通过双层for循环实现的,稍微有点难度的是最里层的if语句。加入列表的格子需要满足以下条件:
(1)tempX(格子的x值)>=0
(2)tempY(格子的y值)>=0
(3)tempX<整个网格的长度
(4)tempY<整个网格的宽度
(5)tempX != nameX或tempY != nameY
前四步保证只有在网格中的格子才会加入列表,这样在网格边缘点击格子时,不会将数值溢出网格长宽的格子错误加入列表。
第五步剔除了点击的格子本身,最后列表中只保留点击的格子周围的格子。
之后,我们要把每个格子的nameX和nameY赋值,可以从生成格子的函数中取得这些值。回到GridManager脚本中,将GenerateGrid函数修改为:
    
    
void GenerateGrid ( ) { for ( int x = 0 ; x < width ; x ++ ) { for ( int y = 0 ; y < height ; y ++ ) { var spawnedTile = Instantiate ( tilePrefab , new Vector3 ( x , y ) , Quaternion . identity ) ; spawnedTile . name = $"Tile { x } { y } " ; GameObject . Find ( $"Tile { x } { y } " ) . GetComponent < Tile > ( ) . nameX = x ; GameObject . Find ( $"Tile { x } { y } " ) . GetComponent < Tile > ( ) . nameY = y ; var isOffset = ( x % 2 == 0 && y % 2 != 0 ) || ( x % 2 != 0 && y % 2 == 0 ) ; spawnedTile . Init ( isOffset ) ; } } cam . transform . position = new Vector3 ( ( float ) width / 2 - 0.5f , ( float ) height / 2 + 1.8f , - 10 ) ; }
最后,回到Unity窗口中,将Grid Manager拖入Tile脚本组件中的Grid Manager栏位。
(五)实现生命游戏规则
规则本身的编程并不难,但要注意,生命游戏是 逐代 演化的,也就是每过一段时间变化一次,所以要在整个游戏过程中规律地重复执行生命游戏规则。之前我尝试过使用协程,但是出现了一些问题,所以我改变了写法,用计时器方法来实现。
开始前,先获得周边格子中被点亮的格子个数,然后应用规则。
首先,在Tile脚本中声明一个代表被点亮格子个数的整型变量。
    
    
public int aliveCellCount ;
接下来,在GridManager脚本中加入以下内容:
    
    
float passedTime = 0 ; public float targetTime = 0.5f ; GameObject [ ] tiles ; List < GameObject > temps ; public void Start ( ) { tiles = GameObject . FindGameObjectsWithTag ( "Tile" ) ; } void Repeat ( ) { if ( passedTime > targetTime ) { for ( int i = 0 ; i < tiles . Length ; i ++ ) { tiles [ i ] . GetComponent < Tile > ( ) . aliveCellCount = 0 ; temps = tiles [ i ] . GetComponent < Tile > ( ) . tempCells ; foreach ( GameObject tile in temps ) { if ( tile . transform . GetChild ( 0 ) . gameObject . activeInHierarchy == true ) { tiles [ i ] . GetComponent < Tile > ( ) . aliveCellCount ++ ; } } } StartRules ( ) ; passedTime = 0 ; } passedTime += Time . deltaTime ; } void StartRules ( ) { for ( int i = 0 ; i < tiles . Length ; i ++ ) { if ( tiles [ i ] . GetComponent < Tile > ( ) . aliveCellCount == 3 ) { tiles [ i ] . transform . GetChild ( 0 ) . gameObject . SetActive ( true ) ; } if ( tiles [ i ] . GetComponent < Tile > ( ) . aliveCellCount == 2 ) { tiles [ i ] . transform . GetChild ( 0 ) . gameObject . SetActive ( tiles [ i ] . transform . GetChild ( 0 ) . gameObject . activeSelf ) ; } if ( tiles [ i ] . GetComponent < Tile > ( ) . aliveCellCount != 3 && tiles [ i ] . GetComponent < Tile > ( ) . aliveCellCount != 2 ) { tiles [ i ] . transform . GetChild ( 0 ) . gameObject . SetActive ( false ) ; } } }
这里还要注意的是,由于脚本中我们使用了寻找标签的方法,所以要将Tile预制体的标签设为Tile。
(六)开始和重启游戏
之前,我们已经做好了游戏的开始按钮和清除按钮。现在,我们要分别写两个按钮事件。当按下开始按钮后,将开始“生命演化”,并禁用鼠标点击。当按下清除按钮时,将清空画布,回到初始状态。
首先,在GridManager脚本中加入以下内容:
    
    
public bool hasStarted ; public void Start ( ) { hasStarted = false ; } void Update ( ) { if ( hasStarted == true ) { Repeat ( ) ; } } public void StartButton ( ) { hasStarted = true ; } public void ClearButton ( ) { hasStarted = false ; foreach ( GameObject tile in tiles ) { tile . GetComponent < Tile > ( ) . isClicked = false ; tile . GetComponent < Tile > ( ) . highlight . SetActive ( false ) ; } }
在Tile脚本中也要加入开始游戏后禁用鼠标点击这个条件。三个鼠标事件都要修改:
    
    
void OnMouseEnter ( ) { if ( GameObject . Find ( "Grid Manager" ) . GetComponent < GridManager > ( ) . hasStarted == false ) { highlight . SetActive ( true ) ; } } void OnMouseExit ( ) { if ( isClicked == false && GameObject . Find ( "Grid Manager" ) . GetComponent < GridManager > ( ) . hasStarted == false ) { highlight . SetActive ( false ) ; } } void OnMouseDown ( ) { if ( GameObject . Find ( "Grid Manager" ) . GetComponent < GridManager > ( ) . hasStarted == false ) { isClicked = ! isClicked ; if ( isClicked == true ) { highlight . SetActive ( true ) ; } else { highlight . SetActive ( false ) ; } } }
最后,回到Unity窗口中,在Start Button和Clear Button中把Grid Manager和对应的按钮事件拖入On Click()的相应栏位。
至此,一个简单的生命游戏就做完啦。点亮专属于你的图案,欣赏它美妙的演化过程吧!
完整脚本:
GridManager
    
    
using System . Collections ; using System . Collections . Generic ; using UnityEngine ; public class GridManager : MonoBehaviour { public int width , height ; float passedTime = 0 ; public float targetTime = 0.5f ; public bool hasStarted ; GameObject [ ] tiles ; List < GameObject > temps ; [ SerializeField ] private Tile tilePrefab ; [ SerializeField ] private Transform cam ; public void Start ( ) { hasStarted = false ; GenerateGrid ( ) ; tiles = GameObject . FindGameObjectsWithTag ( "Tile" ) ; } void Update ( ) { if ( hasStarted == true ) { Repeat ( ) ; } } public void StartButton ( ) { hasStarted = true ; } public void ClearButton ( ) { hasStarted = false ; foreach ( GameObject tile in tiles ) { tile . GetComponent < Tile > ( ) . isClicked = false ; tile . GetComponent < Tile > ( ) . highlight . SetActive ( false ) ; } } void Repeat ( ) { if ( passedTime > targetTime ) { for ( int i = 0 ; i < tiles . Length ; i ++ ) { tiles [ i ] . GetComponent < Tile > ( ) . aliveCellCount = 0 ; temps = tiles [ i ] . GetComponent < Tile > ( ) . tempCells ; foreach ( GameObject tile in temps ) { if ( tile . transform . GetChild ( 0 ) . gameObject . activeInHierarchy == true ) { tiles [ i ] . GetComponent < Tile > ( ) . aliveCellCount ++ ; } } } StartRules ( ) ; passedTime = 0 ; } passedTime += Time . deltaTime ; } void StartRules ( ) { for ( int i = 0 ; i < tiles . Length ; i ++ ) { if ( tiles [ i ] . GetComponent < Tile > ( ) . aliveCellCount == 3 ) { tiles [ i ] . transform . GetChild ( 0 ) . gameObject . SetActive ( true ) ; } if ( tiles [ i ] . GetComponent < Tile > ( ) . aliveCellCount == 2 ) { tiles [ i ] . transform . GetChild ( 0 ) . gameObject . SetActive ( tiles [ i ] . transform . GetChild ( 0 ) . gameObject . activeSelf ) ; } if ( tiles [ i ] . GetComponent < Tile > ( ) . aliveCellCount != 3 && tiles [ i ] . GetComponent < Tile > ( ) . aliveCellCount != 2 ) { tiles [ i ] . transform . GetChild ( 0 ) . gameObject . SetActive ( false ) ; } } } void GenerateGrid ( ) { for ( int x = 0 ; x < width ; x ++ ) { for ( int y = 0 ; y < height ; y ++ ) { var spawnedTile = Instantiate ( tilePrefab , new Vector3 ( x , y ) , Quaternion . identity ) ; spawnedTile . name = $"Tile { x } { y } " ; GameObject . Find ( $"Tile { x } { y } " ) . GetComponent < Tile > ( ) . nameX = x ; GameObject . Find ( $"Tile { x } { y } " ) . GetComponent < Tile > ( ) . nameY = y ; var isOffset = ( x % 2 == 0 && y % 2 != 0 ) || ( x % 2 != 0 && y % 2 == 0 ) ; spawnedTile . Init ( isOffset ) ; } } cam . transform . position = new Vector3 ( ( float ) width / 2 - 0.5f , ( float ) height / 2 + 1.8f , - 10 ) ; } }
Tile
    
    
using System . Collections ; using System . Collections . Generic ; using UnityEngine ; public class Tile : MonoBehaviour { [ SerializeField ] private Color baseColor , offsetColor ; [ SerializeField ] private SpriteRenderer tileRenderer ; public GameObject highlight ; public GameObject GridManager ; public bool isClicked = false ; public int nameX ; public int nameY ; int gridCountX ; int gridCountY ; public int aliveCellCount ; public List < GameObject > tempCells = new List < GameObject > ( ) ; void Start ( ) { gridCountX = GridManager . GetComponent < GridManager > ( ) . width ; gridCountY = GridManager . GetComponent < GridManager > ( ) . height ; for ( int i = - 1 ; i <= 1 ; i ++ ) { for ( int j = - 1 ; j <= 1 ; j ++ ) { int tempX = nameX + i ; int tempY = nameY + j ; if ( tempX < gridCountX && tempX >= 0 && tempY < gridCountY && tempY >= 0 && ( tempX != nameX | tempY != nameY ) ) { tempCells . Add ( GameObject . Find ( $"Tile { tempX } { tempY } " ) ) ; } } } } public void Init ( bool isOffset ) { tileRenderer . color = isOffset ? offsetColor : baseColor ; } void OnMouseEnter ( ) { if ( GameObject . Find ( "Grid Manager" ) . GetComponent < GridManager > ( ) . hasStarted == false ) { highlight . SetActive ( true ) ; } } void OnMouseExit ( ) { if ( isClicked == false && GameObject . Find ( "Grid Manager" ) . GetComponent < GridManager > ( ) . hasStarted == false ) { highlight . SetActive ( false ) ; } } void OnMouseDown ( ) { if ( GameObject . Find ( "Grid Manager" ) . GetComponent < GridManager > ( ) . hasStarted == false ) { isClicked = ! isClicked ; if ( isClicked == true ) { highlight . SetActive ( true ) ; } else { highlight . SetActive ( false ) ; } } } }
游戏演示视频:
Logo

分享前沿Unity技术干货和开发经验,精彩的Unity活动和社区相关信息

更多推荐