前言
开局一张plane,其余靠shader编。本游戏为shader绘制贪吃蛇,没有3D模型,想了解3D版本的开发,可以跳转到【c# Unity贪吃蛇教程】
已经是第五期C#不同平台制作贪吃蛇了,前三期分别是【c# 控制台贪吃蛇教程】、【c# winform贪吃蛇教程】、【c# WPF贪吃蛇教程】
本期用Unity制作shader制作,主要学习shader的网格绘制,shader矩形的移动,shader坐标等内容。
项目教程完全小白化,大神不用瞄了,本项目有源码下载。
目录
一、Shader内容
如果看过前面文章,可以直接跳过准备工作这栏
1.1、创建Shader文件
创建一个Unlit shader
1.2、Shader变量
清除内部内容,写入文件路径和名称
Shader "Custom/DrawGrid"
接着写入背景色,和贪吃蛇二维网格的变量
_backgroundColor("BackgroundColor",Color) = (1.0,1.0,1.0,1.0)//面板背景色
_gridColor("GridColor",Color) = (0.5,0.5,0.5)// 网格的颜色
_lineColor("LineColor",Color) = (0.0,0.0,1.0)//坐标轴的颜色
_tickWidth("TickWidth",Range(0.01,1)) = 0.1 //网格的间距
_gridWidth("GridWidth",Range(0.0001,0.1)) = 0.008//网格的宽度
写入格子宽高
_CellWidth("CellWidth",Range(0.1,1)) = 0.008//格子的宽度
_CellHeight("CellHeight",Range(0.1,1)) = 0.008//格子的高度
写入蛇头部的变量
_HeadCellColor("HeadCellColor",Color)=(1,1,1,1)//蛇头的颜色
_HeadCellCenterPos("HeadCellCenterPos",Vector)=(0.5,0.5,0,0)//蛇头的位置
写入蛇身子变量
_CellColor("CellColor",Color)=(1,1,1,1)//蛇身的颜色
_CellCenterPos("_CellCenterPos",Vector)=(0.5,0.5,0,0)//蛇身的坐标
_CountNum("CountNum",int) = 1//身子格子的数量
写入食物变量
_FoodCellColor("FoodCellColor",Color)=(1,1,1,1)//食物的颜色
_FoodCellCenterPos("FoodCellCenterPos",Vector)=(0.5,0.5,0,0)//食物的坐标
1.3、Shader单个格子绘制
游戏中,我们的蛇,食物需要用到格子,这里我们绘制一个格子方法
float2 Cellrect(float2 pos,float width,float height,float2 center)
{
float2 realp = pos - center;
float2 rectwidth = width * 0.5;
float2 rectheight = height * 0.5;
float hotz = step(-rectwidth.x, realp.x) - step(rectwidth.x, realp.x);
float vert = step(-rectheight.y, realp.y) - step(rectheight.y, realp.y);
return hotz * vert;
}
1.4、Shader的CGPROGRAM
CGPROGRAM 是定义Vertex/Fragment 的地方,它本身是放在Pass块内
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
....略
1.5、Shader顶点着色
顶点结构主要做空间变换以及纹理映射
struct appdata
{
float4 vertex:POSITION;
float2 uv:TEXCOORD0;
};
struct v2f
{
float2 uv:TEXCOORD0;
float4 position:TEXCOORD1;
float4 vertex:SV_POSITION;
};
v2f vert(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.position=v.vertex;
o.uv = v.uv;
return o;
}
POSITION是模型空间下的顶点坐标
SV_POSITION是裁剪空间下的坐标
TEXCOORD是纹理集
UnityObjectToClipPos用于将物体空间的顶点坐标转换到裁剪空间的坐标
1.7、Shader网格绘制
顶点结构主要做空间变换以及纹理映射
//定义网格的的间距
const float tickWidth = _tickWidth;
if (mod((cpos.x + 0.0125) * 2, tickWidth) < _gridWidth)
{
col = gridColor;
}
if (mod((cpos.y + 0.025), tickWidth) < _gridWidth)
{
col = gridColor;
}
1.8、Shader网格绘制
在之前的贪吃蛇中,我们采用了二维数组或者一位数组结合数据类,这里我们直接用shader绘制网格。
//围墙
col = col + saturate(step(i.uv.x, _wallWidth - .025) + step(1 - _wallWidth, i.uv.x - .025) +
step(i.uv.y, _wallWidth) + step(1 - _wallWidth, i.uv.y)) * _wallColor;
saturate(x) 如果x取值小于0,则返回值为0。如果x取值大于1,则返回值为1。若x在0到1之间,则直接返回x的值
step(a,x) 如果x <a 则返回0 如果x>=0 则返回1
1.9、Shader蛇的绘制
//头部格子
float3 headcellpos = float3(_HeadCellCenterPos.x , _HeadCellCenterPos.y , _HeadCellCenterPos.z);
float headcellrect= Cellrect(cpos, _CellWidth, _CellHeight, headcellpos);
float3 hcol = headcellrect * _HeadCellColor;
//身体格子
float bodycellrect = 0;
float3 bodycol = float3(0, 0, 0);
float2 allgridpos = float2(0, 0);
for (int j = 0; j < _CountNum; j++)
{
float3 cellpos = float3(_CellCenterPos.x , _CellCenterPos.y , _CellCenterPos.z);
bodycellrect = Cellrect(cpos, _CellWidth, _CellHeight, cellpos);
bodycol = bodycellrect * _CellColor;
}
//食物格子
float3 foodpos = float3(_FoodCellCenterPos.x - 4.9 + 0.02, _FoodCellCenterPos.y - 4.8 + 0.05, _FoodCellCenterPos.z);
float foodrect = Cellrect(cpos, _CellWidth, _CellHeight, foodpos);
float3 fcol = _FoodCellColor * foodrect;
二、主要功能函数
前面几期已经讲过了,这里说下不同的函数
2.1、地图数据中转
因为shader变量里面没法做数据集合和数据存取,我们依然需要一个数据集合变量作为数据存取地方。
然后就是将数据生成对应的地图数据
private void CreateGrid()
{
int index = 0;
// 创建行
for (int i = 0; i < row; i++)
{
for (int j = 0; j < column; j++)
{
CellData celldata = new CellData();
celldata.cellIndex = index;
celldata.cellVect2 = new Vector3(i, j);
if (i % (row - 1) == 0 || j % (column - 1) == 0)
{
celldata.mapType = 1;
}
else
{
celldata.mapType = 0;
}
mapcelldataList.Add(celldata);
index++;
}
}
}
2.2、蛇的操作
与之前不同,不再用Update ,FixedUpdate作为动作更新,和计时相关操作。这里采用新的方法,协程计时更新游戏画面。
IEnumerator SnakeTime()
{
while (true)
{
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)
yield break;//直接推出更新,确保游戏没有延迟感觉
EatFood(hx,hy);
DrawSanke(hx, hy);
yield return new WaitForSeconds(0.4f);
List<CellData> templist = new List<CellData>();
templist.AddRange(snakecelldataList);
for (int i = 0; i < templist.Count; i++)
{
if (i != 0)
{
Debug.Log(i + " A " + templist[i - 1].cellIndex + " " + templist[i - 1].cellVect2);
Debug.Log(i + " B " + templist[i].cellIndex + " " + templist[i].cellVect2);
snakecelldataList[i] = templist[i - 1];
}
}
bdX = hx;
bdY = hy;
}
}
}
while是循环执行画面更新
yield return new WaitForSeconds(0.4f);每0.4秒更新一次画面
yield break;跳出这个游戏更新
2.3、画蛇
画蛇的方式与之前的课程也不同了,这里采用遍历蛇的集合,来更新蛇的涂色。
void DrawSanke(int x, int y)
{
int count = snakecelldataList.Count - 1;
girdMeshRenderer.material.SetInt("_CountNum", count);
girdMeshRenderer.material.SetColor("_HeadCellColor", snakeHeadColor);
CellData headcell = new CellData();
headcell = GetSnakeCell(x, y); ;
Vector2 vct2 = new Vector2(headcell.cellVect2.y * 0.25f - 4.9f + 0.02f,
headcell.cellVect2.x * 0.5f - 4.8f + 0.05f);
girdMeshRenderer.material.SetVector("_HeadCellCenterPos", new Vector4(vct2.x, vct2.y, 0, 0));
snakecelldataList[0] = headcell;
if (count > 0)
{
Vector4[] gridPositions = new Vector4[count];
for (int i = 0; i < snakecelldataList.Count; i++)
{
if (i != 0)
{
girdMeshRenderer.material.SetColor("_CellColor", snakeBodyColor);
CellData bodycell = snakecelldataList[i];
Vector2 bvct2 = new Vector2(bodycell.cellVect2.y * 0.25f - 4.9f + 0.02f,
bodycell.cellVect2.x * 0.5f - 4.8f + 0.05f);
girdMeshRenderer.material.SetVector("_CellCenterPos", new Vector4(bvct2.x, bvct2.y, 0, 0));
}
}
}
}
Shader.SetColor 设置shader某个变量的颜色
Shader.SetInt 设置shader某个整型变量
Shader.SetVector 设置shader的坐标
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].cellVect2 == (tempList[i].cellVect2))
{
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();
}
绘制食物
void DrawFood()
{
CellData cell = GetSnakeCell((int)foodPoint[0].cellVect2.x, (int)foodPoint[0].cellVect2.y);
girdMeshRenderer.material.SetColor("_FoodCellColor", foodColor);
Vector2 vct2 = new Vector2(cell.cellVect2.y * 0.25f, cell.cellVect2.x * 0.5f);
girdMeshRenderer.material.SetVector("_FoodCellCenterPos", new Vector4(vct2.x, vct2.y, 0, 0));
}
对shader中的变量操作
2.5、吃到食物
吃到食物的方法与之前课程也有不同,改为吃到食物后不立即更新蛇的长度,而是将吃到的食物临时加入集合,后续加长蛇身
吃到食物
void EatFood(int x, int y)
{
if (foodPoint.Count <= 0) return;
//吃到食物
if (x == foodPoint[0].cellVect2.x && y == foodPoint[0].cellVect2.y)
{
eatfoodPointIndexList.Add(foodPoint[0].cellIndex);
CreateFood();
eatFoodNum++;
TextShow();
}
SnakeAdd();
}
加长蛇身
void SnakeAdd()
{
if (eatfoodPointIndexList.Count <= 0)
return;
CellData cellData = new CellData();
cellData = mapcelldataList.Find(x => x.cellIndex == eatfoodPointIndexList[0]);
snakecelldataList.Add(cellData);
eatfoodPointIndexList.RemoveAt(0);
}
三、运行图示
四、总结
- Shader的网格和蛇的信息要与脚本的变量对应,前者是存粹的显示模块,后者才是数据,前者是数据的映射。
- Shader这个模式中,协程比update好用,有兴趣的可以实时task.run。
写着写着就这么多了,可能不是特别全,不介意费时就看看吧。有时间还会接着更新。