前言
不神叨叨了,直接说人话。
已经是第四期C#不同平台制作贪吃蛇了,前三期分别是【c# 控制台贪吃蛇教程】、【c# winform贪吃蛇教程】、【c# WPF贪吃蛇教程】
本期用Unity制作3d网格贪吃蛇,项目教程完全小白化,大神不用瞄了,本项目有源码下载。
目录
先看开发过效果程图
一、准备工作
如果看过前面文章,可以直接跳过准备工作这栏
1.1、创建Resources文件夹
Resources文件夹中可以存放工程的图片,音效,模型等资源,用于读取加载使用
文件夹在Asset下根目录,属于一级目录,不要放到其他文件下面,否则Resources资源读取不到
1.2、创建格子3D模型
右键SampleScence添加一个3D的cube
该cube可以组成二维地图的矩阵点,也是蛇和食物的组件
将该cube从SampleScence中拖拽到Resources文件夹中
1.3、创建shader
创建shader是为了取代cube模型中的默认材质的shader
cube是一个模型,他有默认的材质,我们创建一个新的材质球取代默认材质。
shader,我们可以创建一个属于自己的材质球shader,这方便我们修改渲染。
接着,我们这里需要将默认材质球的shader渲染修改成我们自己创建的shader
最后,再将材质球拖拽到cube的MeshRenderer的材质集合中
1.4、创建类Tanchishe3D
创建类之前,先在SampleScene中GameSrcipt实体,并将创建的Tanchishe3D类,将该文件拖拽到GameSrcipt实体身上
1.5、添加Tag标签
Tag标签主要作为实体的识别标签,方便我们查找物体,我们创建一个标签
1.6、声明全部变量
//2d视角坐标
public Vector3 SHOW2DV3;
//3d视角坐标
public Vector3 SHOW3DV3;
//GameElement类的集合
public List<GameElement> elementList = new List<GameElement>();
//资源加载的顺序
int ResourceIndex = 0;
//蛇身xy
int bdX = 0; int bdY = 0;
//头xy
int hx = 0; int hy = 0;
//移动 步数
int stepNum = 0;
//吃了多少食物
int eatFoodNum = 0;
//格子宽高
public int size = 30;
//行
public int row = 20;
//列
public int column = 40;
//地图坐标
public List<CellData> mapcelldataList = new List<CellData>();
//蛇点集合
public List<CellData> snakecelldataList = new List<CellData>();
//食物坐标
public List<CellData> foodPoint = new List<CellData>();
//移动速度
int moveBasicSpd = 2;
//计时器
float timer = 0;
//是否死亡
public bool isLive = true;
//操作方向
public Dirction mycurDirction = Dirction.D_right;
//UI类
public UImanager uImanager;
/// <summary>
/// 以下是围墙,格子蛇的头,蛇的身子,食物的颜色配置
/// </summary>
public Color cellColor = new Color(128 / 255f, 128 / 255f, 128 / 255f);
public Color wallColor = new Color(255 / 255f, 55 / 255f, 55 / 255f);
public Color snakeHeadColor = new Color(7 / 255f, 150 / 255f, 0);
public Color snakeBodyColor = new Color(225 / 255f, 120 / 255f, 0);
public Color foodColor = new Color(125 / 255f, 0, 175 / 255f);
不用二维数组,采用类的形式记录信息,并将该类加入到集合List中,构建地图数据
二、主要功能函数
2.1、加载资源
采用异步加载Resources实体
void LoadCube()
{
ResourceRequest request = Resources.LoadAsync<GameObject>("Cube");
request.completed += LoadOver;
}
request.completed += LoadOver 读取完成后,再进行后部操作,避免资源未加载完成,游戏系统已经启动,结果出现bug
加载后不要马上生成实体,我们可以将实体临时存到一个类中,这里我们创建一个资源类
[System.Serializable]
public class GameElement
{
public int objID;
public string objName;
public Type objType;
public GameObject obj;
}
在 inspector 面板上显示类的变量,可以用 [System.Serializable]。需要注意的是,这里不用 { get; set; } ,因为用了,在 inspector 面板中不会显示变量。
我们从创建一个管理Resources实体的集合,哪里需要生成,我们就在哪里加载
public List<GameElement> elementList = new List<GameElement>();
准备好后,可与i用来存放读取的资源,放到 elementList 集合中
private void LoadOver(AsyncOperation asyncOperation)
{
GameObject cubeObj = (asyncOperation as ResourceRequest).asset as GameObject;
ResourceIndex++;
GameElement element = new GameElement()
{
objID = ResourceIndex,
objName = cubeObj.name,
objType = cubeObj.GetType(),
obj = cubeObj
};
elementList.Add(element);
CreateGrid();
}
最后在这个函数方法里面创建地图
2.2、地图和围墙
private void CreateGrid()
{
GameElement element = elementList.Find((item) => item.objName == "Cube") as GameElement;
GameObject wallobj = element.obj;
int index = 1;
Color color = new Color();
// 创建行
for (int i = 0; i < row; i++)
{
for (int j = 0; j < column; j++)
{
CellData celldata = new CellData();
celldata.cellIndex = index;
celldata.cellVect3 = new Vector3(i, j, 0);
GameObject obj = Instantiate(wallobj, transform);
if (i % (row - 1) == 0 || j % (column - 1) == 0)
{
celldata.mapType = 1;
SetCellPainting(obj.GetComponent<MeshRenderer>(), "_MianColor", wallColor);
obj.transform.position = new Vector3(j, i, -1);
}
else
{
celldata.mapType = 0;
SetCellPainting(obj.GetComponent<MeshRenderer>(), "_MianColor", cellColor);
obj.transform.position = new Vector3(j, i, 0);
}
celldata.cellObj = obj;
mapcelldataList.Add(celldata);
index++;
}
}
}
obj.transform.position = new Vector3(j, i, -1); 是设置坐标的接口
Instantiate 实例化,生成物体到舞台上
2.3、蛇的操作
创建操作的枚举类型
public enum Dirction
{
D_up,
S_down,
A_left,
D_right
}
操作四个方向,并设置蛇方向
public void KeyDown()
{
if (Input.GetKey(KeyCode.UpArrow))
{
SnakeDirction(Dirction.S_down, Dirction.D_up);
}
else if (Input.GetKey(KeyCode.DownArrow))
{
SnakeDirction(Dirction.D_up, Dirction.S_down);
}
else if (Input.GetKey(KeyCode.LeftArrow))
{
SnakeDirction(Dirction.D_right, Dirction.A_left);
}
else if (Input.GetKey(KeyCode.RightArrow))
{
SnakeDirction(Dirction.A_left, Dirction.D_right);
}
}
键盘按键与winform和wpf不同,unity并不需要事件监听,我们可以直接将KeyDown放入
private void FixedUpdate()
{
KeyDown();
}
禁止车掉头
void SnakeDirction(Dirction d1, Dirction d2)
{
mycurDirction = mycurDirction != d1 ? d2 : d1;
}
2.4、生成食物
void CreateFood()
{
List<CellData> tempList = new List<CellData>(mapcelldataList);
for (int i = 0; i < tempList.Count; i++)
{
for (int j = 0; j < snakecelldataList.Count; j++)
{
if (snakecelldataList[j].cellVect3 == (tempList[i].cellVect3))
{
tempList[i].mapType = 1;
}
}
}
tempList = new List<CellData>(tempList.Where(x => x.mapType == 0).ToList());
var random = new System.Random();
var index = random.Next(tempList.Count);
CellData cell = tempList[index];
foodPoint = new List<CellData> { cell };
DrawFood();
}
方法是,将地图文件取出,临时放到一个集合,再遍历集合与蛇的集合交集,将他们的交集的mapType = 1,
再筛选出不能生成食物的格子集合,随后随机得到集合中的一个元素,这个元素就是食物生成的Vector3坐标
void DrawFood()
{
CellData cell = GetSnakeCell((int)foodPoint[0].cellVect3.x, (int)foodPoint[0].cellVect3.y);
SetCellPainting(cell.cellObj.GetComponent<MeshRenderer>(), "_MianColor", foodColor);
cell.cellObj.transform.position = new Vector3(cell.cellObj.transform.position.x,
cell.cellObj.transform.position.y, -1);
}
2.5、吃到食物
吃到食物后,立即随机在格子可以行动区域生成食物
void EatFood(int x, int y)
{
if (foodPoint.Count <= 0) return;
//吃到食物
if (x == foodPoint[0].cellVect3.x && y == foodPoint[0].cellVect3.y)
{
CellData cellData = new CellData();
cellData = GetSnakeCell(x, y);
snakecelldataList.Add(cellData);
cellData.cellObj.transform.position = new Vector3(cellData.cellObj.transform.position.x,
cellData.cellObj.transform.position.y, -1);
eatFoodNum++;
TextShow();
CreateFood();
}
}
2.6、吃到自己
void HitSelf(int x, int y)
{
if (snakecelldataList.Where(point => point.cellVect3.x == x && point.cellVect3.y == y).Any())
{
isLive = false;
}
}
吃到自己后,在荧幕中央显示结束语,并且将 isLive = false;
isLive 负责跳出游戏的进程,终止游戏进度。
同理,蛇撞到墙效果一样。
2.7、撞到墙
void HitWall(int x, int y)
{
if (x >= row || y >= column || x < 0 || y < 0)
{
isLive = false;
}
}
2.8、控制蛇
在unity中,与winform和wpf不一样了,不用使用Task.Run(() =>,unity可以提供Update,FixedUpdate,LateUpdate三个函数提供实时更新
- Update:随时每帧更新
- FixedUpdate:帧前更新
- LateUpdate:帧后跟新
执行顺序FixedUpdate>Update>LateUpdate
private void RunSnake(float timer)
{
if (isLive == true)//不把isLive放到while循环条件,放这里可以蛇死亡后跳出循环,不会出现延迟效果
{
switch (mycurDirction)
{
case Dirction.D_up:
hx++;
break;
case Dirction.S_down:
hx--;
break;
case Dirction.A_left:
hy--;
break;
case Dirction.D_right:
hy++;
break;
}
HitWall(hx, hy);
HitSelf(hx, hy);
stepNum++;
TextShow();
if (isLive == false) return;//直接推出更新,确保游戏没有延迟感觉
DrawSanke(hx, hy);
bdX = hx;
bdY = hy;
EatFood(hx, hy);
}
}
private void FixedUpdate()
{
KeyDown();
timer -= (Time.deltaTime * moveBasicSpd);
if (timer < 0)//每计时一次,蛇走一次
{
RunSnake(timer);
timer = 1;
}
}
timer -= (Time.deltaTime * moveBasicSpd); 是计时方法,当 timer 小于0则复原到1秒,重新反复的倒计时,实现蛇的秒更新
涂色蛇的头和身子
void DrawSanke(int x, int y)
{
//蛇头格子
CellData curcell = new CellData();
curcell = GetSnakeCell(x, y);
SetCellPainting(curcell.cellObj.GetComponent<MeshRenderer>(), "_MianColor", snakeHeadColor);
curcell.cellObj.transform.position = new Vector3(curcell.cellObj.transform.position.x,
curcell.cellObj.transform.position.y, -1);
//蛇身子格子
CellData bodycell = new CellData();
bodycell = GetSnakeCell(bdX, bdY);
SetCellPainting(bodycell.cellObj.GetComponent<MeshRenderer>(), "_MianColor", snakeBodyColor);
snakecelldataList.Add(bodycell);
bodycell.cellObj.transform.position = new Vector3(bodycell.cellObj.transform.position.x,
bodycell.cellObj.transform.position.y, -1);
//去除尾巴
CellData celldata = snakecelldataList[0];
GameObject endcellobj = celldata.cellObj;
SetCellPainting(endcellobj.GetComponent<MeshRenderer>(), "_MianColor", cellColor);
celldata.cellObj.transform.position = new Vector3(celldata.cellObj.transform.position.x,
celldata.cellObj.transform.position.y, 0);
snakecelldataList.RemoveAt(0);
}
蛇的头部和身子的格子坐标 transform.position 的z坐标设置为 -1,是让蛇突出位置。
GetComponent<MeshRenderer>() 获取格子的材质图层方法
涂色格子函数方法
public void SetCellPainting(MeshRenderer meshreder,string str, Color col)
{
meshreder.material.SetColor(str, col);
}
将 MeshRenderer 的 material 的 shader 进行颜色修改
2.9、Shader
我们写入一个shader,用来为格子渲染
Shader "Custom/Colorshader"
{
Properties
{
_MianColor ("MianColor ",Color)=(1.0,1.0,1.0,1.0)
_LineColor ("LineColor",Color)=(1.0,1.0,1.0,1.0)
_LineWidth ("LineWidth", range(0,0.5)) = 0.1
}
SubShader
{
Tags { "Queue"="Transparent" }
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
fixed4 _MianColor;
fixed4 _LineColor;
fixed _LineWidth;
struct a2v
{
float4 vertex:POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 vertex:SV_POSITION;
float2 uv : TEXCOORD0;
};
v2f vert(a2v v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
fixed4 frag(v2f i):SV_Target
{
fixed3 col = fixed4(0,0,0,1);
col = _MianColor + col + saturate(step(i.uv.x, _LineWidth) + step(1 - _LineWidth, i.uv.x) +
step(i.uv.y, _LineWidth) + step(1 - _LineWidth, i.uv.y)) * _LineColor;
return fixed4(col,1.0);
}
ENDCG
}
}
}
saturate(x) 函数作用是如果x取值小于0,则返回值为0。如果x取值大于1,则返回值为1。若x在0到1之间,则直接返回x的值
step(x,y) 函数作用是如果y大于x,则step()函数返回1.0;如果y小于等于x,则返回0.0
三、其他功能函数
3.1、ugui类
存放游戏数据的UI类型,内容创建三个文本UI
public class UImanager : MonoBehaviour
{
public Text myText1;
public Text myText2;
public Text myText3;
}
void TextShow()
{
uImanager.myText1.text = "移动 步数:" + stepNum;
uImanager.myText2.text = "吃食物数量:" + eatFoodNum;
uImanager.myText3.gameObject.SetActive(!isLive);
uImanager.myText3.text = isLive == false ? "游戏结束!" : string.Empty;
}
myText3默认设置为隐藏状态,unity中隐藏GameObject,取消勾选即可
3.2、3d视角显示
Camera.main.transform.position = SHOW3DV3;
Camera.main.transform.eulerAngles = new Vector3(-15, 0, 0);
Camera.main 是我们的主摄像头,将摄像头的坐标修改,并将 eulerAngles 仰角修改,即可得到一个3D视角
四、运行图示
五、总结
- 比起winform和wpf原生开发游戏,unity是专业的游戏开发软件,渲染有自己的渲染系统,自己的帧系统。
- Resources加载资源,需要 将资源放置在该文件夹下,也可以在Resources下创建子文件夹分类管理。
- unity的键盘按键不需要添加事件进行监听,按键方法可以直接放在Update,FixedUpdate,LateUpdate三个函数中。
- shader可以进行动态修改,即渲染可以进行实时更新。
- 物体上可以挂在脚本,脚本需要继承MonoBehaviour才能挂载。
写着写着就这么多了,可能不是特别全,不介意费时就看看吧。有时间还会接着更新。