参考了b站的一个视频,不过我记得那个视频也是参考的别的平台…
主要介绍随机墙壁、打雷功能。
代码方面介绍工厂设计模式、协程。
此游戏中的伤害系统暂时就不多介绍了。注意一下多人化时要通过photonView.IsMine区分,让人物的真实玩家那边扣分就可以了。僵尸的扣血则是由主机负责,僵尸血量可以通过实现IPunObserver接口来同步(详情见官网的射击同步)。
Unity 通过CreateAssetMenu为使用自定义资源(.asset) 添加 Assets 菜单按钮,详细请看这里
[CreateAssetMenu(fileName = "MapFactory", menuName = "Factory/MapFactory")]
public class MapFactory : FactorySO<GameObject>
{
//unity的工厂设计模式,需要获得floor和barrier的地方可以通过这个工厂.create...()获得
public GameObject floorPrefab;
public GameObject BarrierPrefab;
public override GameObject Create()
{
GameObject floorInstance = Instantiate(floorPrefab);
return floorInstance;
}
public GameObject CreateBarrier()
{
GameObject BarrierInstance = Instantiate(BarrierPrefab);
return BarrierInstance;
}
}
//应用时:
GameObject newFloor = MapFactory.Create();//创建一个floor实体
可以通过墙壁的位置修改其材质颜色使之具有渐变色,可能更加美观。
墙壁之间高度不一增加了层次感。但是可能看起来比较乱。
墙壁移动用了dotween补间动画插件,动画更加流畅。
public void UpdateBarrier()
{
RandomMaze randomMaze = new RandomMaze((int)mapSize.y, (int)mapSize.x);//初始化一个迷宫对象
randomMaze.BarrierMap(0, (int)mapSize.y , (int)mapSize.x , 0);//随机生成墙壁(迷宫)
//迷宫暂时是以二维bool型数组--Maze记录的,false为墙壁,true为通路。生成迷宫后强制使中心点打通不允许有墙壁。
randomMaze.Maze[(int)(mapSize.y / 2), (int)(mapSize.x / 2 )] = true;
randomMaze.Maze[(int)(mapSize.y / 2 - 1), (int)(mapSize.x / 2) - 1] = true;
randomMaze.Maze[(int)(mapSize.y / 2 - 1), (int)(mapSize.x / 2)] = true;
randomMaze.Maze[(int)(mapSize.y / 2 - 1), (int)(mapSize.x / 2) + 1] = true;
randomMaze.Maze[(int)(mapSize.y / 2), (int)(mapSize.x / 2) - 1] = true;
randomMaze.Maze[(int)(mapSize.y / 2), (int)(mapSize.x / 2) + 1] = true;
randomMaze.Maze[(int)(mapSize.y / 2) + 1, (int)(mapSize.x / 2) - 1] = true;
randomMaze.Maze[(int)(mapSize.y / 2) + 1, (int)(mapSize.x / 2)] = true;
randomMaze.Maze[(int)(mapSize.y / 2) + 1, (int)(mapSize.x / 2) + 1] = true;
GameObject barrierPosition = Instantiate(BarrierParent, this.transform);//创建墙壁父物体,位置和Map对象一样
barrierPosition.transform.localPosition -= new Vector3(0, 1.5f, 0);//位置往下移1.5个单位
currentBarriers = barrierPosition;//这俩都是指这个父物体了,前者是全局变量用以之后控制墙壁的消失,后者是私有变量无法在这个方法外调用。
for (int i = 0; i < mapSize.y; i++)
{
for (int j = 0; j < mapSize.x; j++)
{
if(randomMaze.Maze[i,j]==false)//找到应该生成墙壁的地方
{
GameObject newBarrier = MapFactory.CreateBarrier();//工厂创建墙壁
newBarrier.transform.SetParent(barrierPosition.transform);//设置墙壁的父物体是这个东西
//随机墙壁高度以参差不齐增加层次感,0~0.5,对应这个方法最后的0.5,刚好能使墙壁不会浮空(tips:物体transform设置的是几何中心)
float randomHeight = (float)UnityEngine.Random.Range(0, 5) / 10;
Vector3 NewPos = floorPosList[i, j].GetComponent<Floor>().FloorPos;//提取出这个地板对应的位置
newBarrier.transform.localPosition = NewPos;//设置上这个位置
newBarrier.transform.localScale = new Vector3(1 - outlinePercent, 1, 1 - outlinePercent);//x和z缩放0.9
//设置墙壁高度,可以追过去看代码,设置了相反高度--中心点在地面以下
//后面还有个整体上浮的方法,这样就理解为什么一开始把这些方块设的比较低(高度设为负)了
newBarrier.GetComponent<Barrier>().SetBarrier(randomHeight);
//SetBarrier里用了invoke,有时候巨好用!!!之前不知道然后选择其他方式写好久
#region colorSet
float colorPercentage;//这个region里写的是根据墙壁从左到右的距离,进行一个外观颜色渐变
colorPercentage =j/mapSize.x;
MeshRenderer meshRender = newBarrier.GetComponent<MeshRenderer>();
meshRender.material.color = Color.Lerp(initialColor, endColor, colorPercentage);
#endregion
}
}
}
barrierPosition.transform.DOMoveY(0.5f, 3f).SetEase(Ease.OutCubic);//y方向上的平移。应该是持续3秒的向上平移!到!0.5√√√
}
//方法作用:随机墙壁。
//大致思路是切割此方形地图,十字刀切割。切出左上、右上、左下、右下四个板块,对这四个方形子板块递归切割直到板块过小无法切割。
//解释:切割处生成墙壁。
public void BarrierMap(int topIndex, int bottomIndex, int rightIndex, int leftIndex)
{
int rowIndex, cloumnIndex;
int top = topIndex, bottom = bottomIndex, right = rightIndex, left = leftIndex;
if (right - left < 2 || bottom - top < 2) //当切割到只剩一列时就return---停止递归的条件。
{
return;
}
//只能在偶数行生成墙壁,为了确保有道路 [这行注释是作者写的←]
//TODO 让地图更自然
do
{
cloumnIndex = UnityEngine.Random.Range(left, rightIndex);//列
rowIndex = UnityEngine.Random.Range(top, bottomIndex);//行
}
while ((rowIndex + 1) % 2 != 0 || (cloumnIndex + 1) % 2 != 0);//随机出两个奇数下标(地图第偶数行),然后以此坐标为中心的十字路线全为false
for (int i = left; i < right; i++)
{
Maze[rowIndex, i] = false;
}
for (int j = top; j < bottom; j++)
{
Maze[j, cloumnIndex] = false;
}
//setDoor::“设置门、打通墙壁”,覆盖此中心点的随机大小上四个边界处打通两个墙壁
//可以理解为:上面代码生成了一个十字形墙壁,我拿一个随机大小的桶盖住这个十字的中心点,然后碰到桶壁的那4个地方被压扁不再有墙壁--形成通路(即门)。
SetDoor(top, bottom, right, left, rowIndex, cloumnIndex);
//递归继续切割被这个十字切出来的四个小版块。
BarrierMap(top, rowIndex, cloumnIndex, left);
BarrierMap(rowIndex + 1, bottom, cloumnIndex, left);
BarrierMap(top, rowIndex, rightIndex, cloumnIndex + 1);
BarrierMap(rowIndex + 1, bottom, rightIndex, cloumnIndex + 1);
}
void SetDoor(int topIndex, int bottomIndex, int rightIndex, int leftIndex,int rowIndex,int columnIndex)
{
int top,bottom,right,left;
do
{
top = UnityEngine.Random.Range(topIndex, rowIndex);
bottom = UnityEngine.Random.Range(rowIndex, bottomIndex);
left = UnityEngine.Random.Range(leftIndex, columnIndex );
right = UnityEngine.Random.Range(columnIndex, rightIndex);
}
while (top % 2 != 0 || bottom % 2 != 0 || left % 2 != 0 || right % 2 != 0);
//在当前规定的范围以及其中这个点随机出一个覆盖此点的小范围,且边界都是偶数
//在这随机出来的小范围上:
//这一行上从左右边界开始各向内打通两个点
Maze[rowIndex,right] = true;
Maze[rowIndex, right-1] = true;
Maze[rowIndex, left] = true;
Maze[rowIndex, left+1] = true;
//这一列上从上下边界开始各向内打通两个点
Maze[top, columnIndex] = true;
Maze[top+1, columnIndex] = true;
Maze[bottom-1, columnIndex] = true;
Maze[bottom, columnIndex] = true;
}
可以跟随玩家也可以随机。
这是随机的情况,随机在一个通路的地方生成闪电,多端同步,所以在通知其他端生成闪电时随机就已经确定了。
public void GenerateLightning()//闪电
{
int randomX = 0;
int randomZ = 0;
while (true)
{
randomX = Random.Range(0, 25);
randomZ = Random.Range(0, 25);
if(theMaze == null)
{
return;
}
else if(theMaze.Maze[randomX, randomZ])//是通路,生成闪电
{
break;
}
}
//确定随机后再同步,而不是让每个客户端都去随机
photonView.RPC("LightningRPC", RpcTarget.All, randomX, randomZ);
}
[PunRPC]
private void LightningRPC(int x,int z)
{
StartCoroutine("Lightning", new object[] { x,z});
}
//协程,控制闪电、音效、地板提示等
IEnumerator Lightning(object[] paras)
{
int x = (int) paras[0];
int z = (int) paras[1];
float spawnDelay = 2f, tileFlashSpeed = 4, timer = 0;
Transform tile = GetTileByPlayerInt(x, z);
if (tile != null)
{
//地板颜色闪烁提示
Material tileMat = tile.GetComponent<MeshRenderer>().material;
Color originColor = tileMat.color;
Color flashColor = Color.red;
while (timer < spawnDelay)
{
tileMat.color = Color.Lerp(originColor, flashColor, Mathf.PingPong(timer * tileFlashSpeed, 1));
timer += Time.deltaTime;
yield return null;
}
//音效及模型
AudioManager.instance.Thunder(tile.position);
GameObject lightning = Instantiate(punLightningPrefab, Vector3.zero, Quaternion.identity);
GameObject collisionCube = Instantiate(punCollisionCubePrefab, tile.position, Quaternion.identity);
lightning.GetComponent<LightningBoltScript>().StartPosition = new Vector3(10, 8, 5);
lightning.GetComponent<LightningBoltScript>().EndPosition = tile.position;
tileMat.color = originColor;
}
}
Color.Lerp
是插值运算,也会使颜色变化更加流畅。
变化之后别忘了改回原先的颜色。
声明originColor
(Color)的时候rgb参数是float的,与255的不匹配。所以单独记录一下原color,后再用其赋值即可。
最后部分是闪电预制体的生成,此游戏的伤害判断是闪电的地方会生成一个碰撞Cube,通过碰撞来检测是否闪电击中了玩家,毕竟闪电是个粒子特效,没有碰撞效果。
DoTween
插件,大家可以去DoTween官网学习。Unity官方资源中是免费的。
使用pun提供的rpc,以上闪电的生成等内容均需要rpc功能同步。
通过photonView.RPC(...)
发起请求。
第一个参数是要执行那个方法的方法名,第二个参数是rpc对象。
别忘了在那个方法头部加上[PunRPC]
以让他支持rpc调用。
这个游戏是开发内容比较多的一个游戏。
包含生命值系统、僵尸的AI组件寻路、闪电(可以通过控制灯光和音效制造完美气氛)、枪械控制、墙壁的生成等。
部分代码采用单例模式和工厂设计模式。子弹可以采用对象池进行优化。