如何使用Unity实现“饥荒”游戏中的效果

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何使用Unity实现“饥荒”游戏中的效果相关的知识,希望对你有一定的参考价值。

Don\'t starve根本就不是45度格子拼出来的isometric 2D游戏。。。以上回答方向全部错了。。。真是的,这些人到底有没有玩过这游戏啊ˊ_>ˋ。

Don\'t starve是一个3D的场景,所有的物体都是由billboard渲染出来的。它场景中所有的物体模型都是一个单独的面片,它们本就是是平面风格而不是isometric风格,根本就不是45度格子拼出来的。
它的视角可以切换,切换之后场景里除了地面之外所有物体的外观完全没有丝毫改变,这压根就是billboard。这些billboard的旋转中心(或者说local坐标的原点)在y=0的位置上,所以一棵草,一个房子,甚至是构成墙体的一根柱子,它都是绕着扎根于地面的那一点旋转的一个面片,方向永远朝着玩家。
这是3D世界,有z坐标的,排序按照z坐标来啊摔(╯°□°)╯︵ ┻━┻。有Alpha blending自己排没Alpha blending用z-buffer排啊(╯°□°)╯︵ ┻━┻哪个3D引擎都给你做好了不用你手写的啊喂!
参考技术A 饥荒属于2D游戏范围,但是视角看上去非常像是3D游戏。

Unity ECS实现RTS游戏中的游戏单位框选集结和移动控制

今天想给大家分享的主题是如何实现RTS类型游戏中的游戏单位角色控制

本文中会介绍如何运用最新的ECS架构来实现游戏单位控制

效果演示

效果实现

选中多个游戏单位

public class UnitControlSystem : ComponentSystem 

		private float3 startPosition;
		protected override void OnUpdate() // OnUpdate与MonoBehaviour中的UPdate一样,游戏运行的每一帧都会执行OnUpdate
		
				if(Input.GetMouseButtonDown(0)) // 鼠标左键按下时执行的内容
				
						// Mouse Pressed
						StartPosition = UnilsClass.GetMouseWorldPosition(); // 记录鼠标按下的位置
				
				if(Input.GetMouseButtonUP(0)) //鼠标左键弹起时执行的内容
				
						// Mouse Released
						float3 endPosition = UnilsCalss.GetMouseWorldPosition(); // 记录鼠标弹起的位置
						
						float3 lowerLeftPosition = new float3(math.min(startPosition.x, endPosition.x),
								math.min(startPosition.y, endPosition.y),0); // 获取鼠标框选方框左下角的位置
						float3 upperRightPosition = new float3(math.max(startPosition.x, endPosition.x),
								math.max(startPosition.y, endPositon.y),0); // 获取鼠标框选方框右上角的位置
						
						Entities.ForEach((Entity entity, ref Translation translation) => 
								float3 entityPosition = translation.Value;
								if(entityPosition.x >= lowerLeftPosition.x &&
									 entityPosition.y >= lowerLeftPosition.y &&
									 entityPosition.x <= upperRightPosition.x &&
									 entityPosition.y <= upperRightPosition.y)
									 Debug.Log(entity);
								
						); // 遍历所有实体,判断它是否被框选选中
				
		

  • 上方代码实现的功能是获取被鼠标框选的游戏单位,如果需要源代码可以在文末添加爱丽丝老师的QQ或者微信号领取
  • 代码讲解
  • 获取鼠标框选方框的左下角和右上角
float3 lowerLeftPosition = new float3(math.min(startPosition.x, endPosition.x),
math.min(startPosition.y, endPosition.y),0); // 获取鼠标框选方框左下角的位置
float3 upperRightPosition = new float3(math.max(startPosition.x, endPosition.x),
math.max(startPosition.y, endPositon.y),0); // 获取鼠标框选方框右上角的位置
  • 鼠标在按下和弹起的过程中画出的方框一般存在两种情况
    • 鼠标的起始位置对应左下角,终止位置对应右上角
    • 鼠标按下时的起始位置是右上角,终止位置则是左下角
  • 在计算方框的起始位置为右上角,终止位置为左下角时不能直接用起始位置当方框的左下角,要把终止位置当做方框左下角的位置
  • 想要统一的获得左下角和右上角的位置需要写一些算法,如上方代码所示,这个算法很简单,就是比较起始位置和终止位置,取较小值作为左下角点,然后用两者的较大值作为右上角点
  • 查找被选中的游戏实体
Entities.ForEach((Entity entity, ref Translation translation) => 
								float3 entityPosition = translation.Value;
								if(entityPosition.x >= lowerLeftPosition.x &&
									 entityPosition.y >= lowerLeftPosition.y &&
									 entityPosition.x <= upperRightPosition.x &&
									 entityPosition.y <= upperRightPosition.y)
									 Debug.Log(entity);
								
						);
  • ForEach方法的作用是遍历每一个游戏单位,后面的Lambda表达式的功能是判断游戏单位的位置坐标是否在鼠标框选范围内,并打印鼠标范围框选范围内的游戏单位

绘制选区

  • 在场景中创建一个空节点,起名为SelectionArea(选择区域),再创建一个空子节点,取名为Sprite(精灵节点)
  • 为精灵节点添加精灵渲染器,并选择一张绿色的图片(把白色图片设置成绿色也可以)
  • 这里需要注意一下这张图片的大小
  • 我们为这张图片设置了0.5的偏移值,这是什么意思呢?

  • 也就是说SelectionArea(选择区域)节点和Sprite(精灵)节点的位置关系变成了上图的样子,上图中红色坐标轴的原点就是SelectionArea(选择区域)节点的位置,蓝色坐标轴的原点则代表Sprite(精灵)节点的位置,这样偏移以后,将来拖拽、缩放选框时就会以红颜色的中心点为起点,会比较方便
  • 在代码中实现动态绘制选框
public struct UnitSelected : IComponentDate 


public class UnitControlSystem : ComponentSystem 

		private float3 startPosition;
		protected override void OnUpdate() // OnUpdate与MonoBehaviour中的UPdate一样,游戏运行的每一帧都会执行OnUpdate
		
				if(Input.GetMouseButtonDown(0)) // 鼠标左键按下时执行的内容
				
						// Mouse Pressed
						ECS_RTSControls.instance.selectionAreaTransform.gameObject.SetActive(true); // 鼠标按下时激活选区
						startPosition = UnilsClass.GetMouseWorldPosition(); // 记录鼠标按下的位置
						ECS_RTSControls.instance.selectionAreaTransform.position = startPosition; // 设置选取的位置为鼠标按下的位置
				
				
				if(Input.GetMouseButton(0)) // 鼠标左键按下后,拖拽鼠标时要执行的内容
				
						// Mouse Held Down
						float3 selectionAreaSize = (float3)UtilsClass.GetMouseWorldPosition() - startPositon; // 获取鼠标绘制出的选区大小
						ECS_RTSControls.instance.selectionAreaTransform.localScale = selectionAreaSize; // 设置选区大小
				
				
				if(Input.GetMouseButtonUP(0)) //鼠标左键弹起时执行的内容
				
						// Mouse Released
						ECS_RTSControls.instance.selectionAreaTransform.gameObject.SetActive(false); // 在鼠标抬起时隐藏选区
						float3 endPosition = UnilsCalss.GetMouseWorldPosition(); // 记录鼠标弹起的位置
						
						float3 lowerLeftPosition = new float3(math.min(startPosition.x, endPosition.x),
								math.min(startPosition.y, endPosition.y),0); // 获取鼠标框选方框左下角的位置
						float3 upperRightPosition = new float3(math.max(startPosition.x, endPosition.x),
								math.max(startPosition.y, endPositon.y),0); // 获取鼠标框选方框右上角的位置
						
						Entities.ForEach((Entity entity, ref Translation translation) => 
								float3 entityPosition = translation.Value;
								if(entityPosition.x >= lowerLeftPosition.x &&
									 entityPosition.y >= lowerLeftPosition.y &&
									 entityPosition.x <= upperRightPosition.x &&
									 entityPosition.y <= upperRightPosition.y)
									 PostUpdateCommands.AddComponent(entity, new UnitSelected()); // PostUpdateCommands.AddComponent是ECS里的API,
																																								// 它在这里的功能是为被选中的游戏对象添加UnitSelected组件
							  
						); // 遍历所有实体,判断它是否被框选选中
				
		

  • 注意
    • SelectionArea节点被添加到总控脚本ECS_RTSControls里了,所以在上方代码中是通过访问总控脚本的单例来获取SelectionArea节点
  • 此脚本是在之前的UnitControlSystem脚本上增加了一个类和一些代码
    • 这些代码会在鼠标按下时激活SelectionArea节点,并将SelectionArea节点的位置设置为鼠标按下的位置
    • 在鼠标拖拽过程中会不断获取鼠标当前位置,并用鼠标当前为减去鼠标初始位置,以得到当前鼠标框选的选区大小,然后赋值给selectionArea的localScale,这样就实现了selectionArea选区随着鼠标拖拽自动改变大小的效果
    • 最后在鼠标抬起时会将selectionArea的SetActive设置为false,隐藏鼠标选区,并为被选中的游戏对象添加UnitSelected结构体
  • 效果演示

绘制角色脚下的圆圈

public class UnitSelectedRenderer : ComponentSysten

		protected override void OnUpdate()
		
				Entities.WithAll<UnitSelected>().ForEach((ref Translation translation) =>  // 通过for循环找到所有带有UnitSelected标记的游戏对象
						float3 position = translation.Value + new float3(0, -3f , +1); // 调低圆圈高度,使它出现在游戏对象脚下
						Graphics.DrawMesh(    
								ECS_RTSControls.instance.unitSelectedCircleMesh, // 通过ECS_RTSControls单例获取圆圈的网格模型
								translation.Value, // 指定圆圈位置为士兵所在的位置
								Quaternion.identity, // 指定旋转为不进行旋转
								ECS_RTSControls.instance.unitSelectedCircleMaterial, // 通过ECS_RTSControls的单例获取圆圈的材质
								0 // 指定要绘制的层
						); // Graphics.DrawMesh是Unity的底层接口,在这里用来绘制游戏角色脚底的圆圈
				);
		

  • 注意
    • 用于绘制角色脚下圆圈的材质添加到了总控脚本ECS_RTSControls里,所以在上方代码中是通过访问总控脚本的单例来获取圆圈的材质的
    • 圆圈的网格模型则是在ECS_RTSControls调用Unity动画的创建网格方法动态创建的,所以也通过ECS_RTSControls的单例获取
  • unitSelectedCircleMaterial(角色选中材质)

如果需要项目源码或资源可以在文末通过添加爱丽丝老师的QQ获取

  • 效果演示

  • 问题

    • 问题1:无法取消选中
      • 上面的代码在选中了左边的角色后,点击空处或再次选中其他角色时并不会取消之前被选中角色的选中状态,为了要解决这个问题我们添加了几行代码,让UnitControlSystem脚本在鼠标弹起时遍历所有游戏对象,删除它们身上的UnitSelected组件
     if(Input.GetMouseButtonUP(0)) //鼠标左键弹起时执行的内容
     
          	// Mouse Released
          	ECS_RTSControls.instance.selectionAreaTransform.gameObject.SetActive(false); // 在鼠标抬起时隐藏选区
          	float3 endPosition = UnilsCalss.GetMouseWorldPosition(); // 记录鼠标弹起的位置
          						
          	float3 lowerLeftPosition = new float3(math.min(startPosition.x, endPosition.x),
          			math.min(startPosition.y, endPosition.y),0); // 获取鼠标框选方框左下角的位置
          	float3 upperRightPosition = new float3(math.max(startPosition.x, endPosition.x),
          			math.max(startPosition.y, endPositon.y),0); // 获取鼠标框选方框右上角的位置
          						
          	Entities.WithAll<UnitSelected>().ForEach((Entity entity) => 
          			PostUpdateCommands.RemoveComponent<UnitSelected>(entity);
          	); // 在鼠标弹起时遍历所有游戏对象,删除它们身上的UnitSelected组件
          						
          	Entities.ForEach((Entity entity, ref Translation translation) => 
          			float3 entityPosition = translation.Value;
          			if(entityPosition.x >= lowerLeftPosition.x &&
          				entityPosition.y >= lowerLeftPosition.y &&
          				entityPosition.x <= upperRightPosition.x &&
          				entityPosition.y <= upperRightPosition.y)
          				PostUpdateCommands.AddComponent(entity, new UnitSelected()); // PostUpdateCommands.AddComponent是ECS里的API,它在这里的功能是为被选中的游戏对象添加UnitSelected组件
          			
          	); // 遍历所有实体,判断它是否被框选选中
     
          ```
          
    
  • 问题2:无法通过点击选中游戏对象

    • 上面代码实现的选中效果必须要把游戏对象整体框入才能选中,但在RTS游戏里,游戏玩家对于单个游戏对象是可以通过点击选中的,而且框选也很麻烦,那怎样才能让它可以点中选中一个对象呢?方法很简单
    • 就是扩大最小选区,选择区域在点击时自动扩大一圈,这样就能确保点击选中单个角色
         float selectionAreaMinSize = 10f; // 鼠标选区最小值
         float selectionAreaSize = math.distance(lowerLeftPosition, upperRightPosition); // 获取当前鼠标选区大小
         if(selectonAreaSize < selectionAreaMainSize) // 检测当前鼠标选区大小是否小于鼠标选区最小值
         
         		lowerLeftPosition += new float(-1, -1, 0) * (selectionAreaMinSize - selectionAreaSize) * .5f; // 将鼠标选区左下角向下拉伸
         		upperRightPosition += new float(+1, +1, 0) * (selectionAreaMinSize - selectionAreaSize) * .5f; // 将鼠标选区右上角向上拉伸
         
    
  • 注意:

    • 这些代码会在鼠标抬起时执行,它会检测当前鼠标选区大小是否小于鼠标选区最小值,如果小于则会根据鼠标选区最小值减去当前鼠标选区大小的值放大鼠标选区
  • 这样就保证了最小区域是足够大的,可以通过点击选中游戏单位

让游戏对象向指定的方向移动

if(Input.GetMouseButtonDown(1)) // 在鼠标右键按下时执行的内容

		Entities.WithAll<UnitSelected>().ForEach((Entity entity, ref MoveTo moveTo) => 
				moveTo.position = UtilsClass.GetMouseWorldPosition(); // 设置移动目标点
				moveTo.move = true;	// 开始移动
		); // 查找所有被选中的游戏对象,设置它们的移动目标点并开始移动

  • 注意:
    • 这段代码会在鼠标右键抬起时执行,它会为所有被选中的游戏对象设置移动目标点,并使游戏对象向目标点移动
    • MoveTo是一个移动脚本,MoveTo的position代表游戏角色移动的目标点,move代表是否开始移动,如果需要完整的ECS源码资源,可以在添加爱丽丝老师领取
  • 效果演示

实现游戏单位按阵列移动

  • 上面实现的效果还有一个问题存在

  • 可以看到上面的两个角色变成一个了,因为如果他们的移动目标点是相同的,那么这两个角色在移动时就会重叠起来
if(Input.GetMouseButtonDown(1)) // 在鼠标右键按下时执行的内容

		float targetPosition = UtilsClass.GetMouseWorldPosition(); // 获取鼠标点击位置
		List<float3> movePositionList = new List<floa3>
		
				targetPosition,
				tragetPosition + new float3(10,0,0),
				tragetPosition + new float3(20,0,0),
				tragetPosition + new float3(30,0,0),
		; // 游戏单位的移动位置列表
		int positionIndex = 0;  // 移动位置列表的位置索引值
		Entities.WithAll<UnitSelected>().ForEach((Entity entity, ref MoveTo moveTo) => 
				moveTo.position = movePositionList[positionIndex]; // 设置移动目标点
				positionIndex = (positionIndex + 1) % movePositionList.Count;
				moveTo.move = true;	// 开始移动
		); // 查找所有被选中的游戏对象,设置它们的移动目标点并开始移动

  • 上面代码中的movePositionList是游戏单位的移动位置列表,当鼠标右键按下设置目标点时,这段代码会遍历所有被选中的游戏单位,并使用positionIndex(位置索引)取出移动位置列表里计算好的移动位置,让这些游戏单位的移动位置都不一样

  • 效果演示

  • 可以看到被选中的游戏单位朝着同一个目标点移动,并且位置都各不相同,这是因为目标点在movePositionList经过处理后,产生四个位置不同的坐标点,这样赋值给游戏单位的就是位置不同的坐标点了

  • 这段代码的问题也很明显:当玩家选中四个以上的游戏单位进行移动时,仍然会产生重叠现象

    • 这是因为movePositionList里的元素只有四个,当这段代码遍历完所有元素时,就会从movePositionList的起始位置重新遍历,所以多出来的游戏单位位置会与其他游戏单位重叠
  • 解决这个问题最简单的方法就是增加movePositionList里的元素个数,让元素个数始终大于游戏单位个数,这个问题自然迎刃而解

  • 不过在一些游戏单位数量动辄就是几十、几百上下的RTS游戏中,这种方法就不够看了,需要用另一种方法

private List<float3> GetPositionListAround(float3 position, float distance, int positionCount)

		List<float3> positionList = new List<float3>(); // 创建一个float3列表
		for (int i = 0; i < positionCount; i++)
		
				int angle = i * (360 / positionCount); // 用位置数量除以360以获得第i个位置在圆环上的角度
				float3 dir = ApplyRotationToVector(new float3(0,1,0), angle); 
				float3 position = startPosition + dir * distance; // 通过dir*distance获取长度为distance的向量,然后加上中心位置以得到向量的实际位置
				positionList.Add(position);
		
		return positionList;


private float3 ApplyRotationToVector(float3 vec, float angle)

		return Quaternion.Euler(0,0,angle) * vec;

  • 上面的GetPositionListAround会返回一个位置列表,位置列表里的所有元素都会以该方法的position参数为圆心,distance为半径呈圆形排列(如下图),这些元素的数量就是positionCount

  • ApplyRotationToVector的作用则是通过传入的角度(angle)来构造旋转值,并使用这个旋转值旋转向量(vec),旋转到了angle所代表的角度,这个方法背后的数学原理,本文就不去细讲了,因为内容很多,如果想要学习这方面的知识,可以在文末添加爱丽丝老师了解
  • 对之前的代码进行修改,使用GetPositionListAround生产位置列表
if(Input.GetMouseButtonDown(1)) // 在鼠标右键按下时执行的内容

		float targetPosition = UtilsClass.GetMouseWorldPosition(); // 获取鼠标点击位置

		List<float3> movePositionList = GetPositionListAround(targetPosition, 10, 5); // 游戏单位的移动位置列表

		int positionIndex = 0;  // 移动位置列表的位置索引值
		Entities.WithAll<UnitSelected>().ForEach((Entity entity, ref MoveTo moveTo) => 
				moveTo.position = movePositionList[positionIndex]; // 设置移动目标点
				positionIndex = (positionIndex + 1) % movePositionList.Count;
				moveTo.move = true;	// 开始移动
		); // 查找所有被选中的游戏对象,设置它们的移动目标点并开始移动

  • 效果演示

  • 由于在上面的代码中GetPositionListAround只指定了5个元素数量,所以重叠的现象依然存在,代码还需要继续改进

private List<float3> GetPositionListAround(float

以上是关于如何使用Unity实现“饥荒”游戏中的效果的主要内容,如果未能解决你的问题,请参考以下文章

游戏开发实战Unity实现类似GitHub地球射线的效果(LineRenderer | 贝塞尔曲线)

游戏开发实战Unity实现类似GitHub地球射线的效果(LineRenderer | 贝塞尔曲线)

游戏开发实战Unity实现类似GitHub地球射线的效果(LineRenderer | 贝塞尔曲线)

如何使用Unity做游戏中的寻路导航

如何在unity中实现拖尾效果

Unity3D 灵巧小知识点☀️ | Unity 中如何让 Toggle组件 实现多选一的效果