发表于: 6/12/2022
如果有人看了上一篇,可能已经稍微有点感觉,我们要做的是不是RTS游戏呢?——很难说不是。
参演物体
依照惯例,首先创建一个{竞技场^Arena}作为整场游戏的地图。 Boundary作为地图边界限制物体移动。 {战舰^Warship}作为Prefab自然就是单位啦。
:::default no-icon 此处也能看到在上一篇中未展示的{小地图指示器^Map Indicator} :::
框选单位
单位选择的思路
选中单位的视觉标识
单位具有被选中属性
鼠标框选
框体素材、跟随鼠标
鼠标左键点击与拖动框选的统一
被框选、点击物体的判断
单位 - Warship
给绑定到单位的脚本中添加如下内容
public bool IsSelected { get; set; } = false;
void Update()
{
selectionIndicator.SetActive(IsSelected);
}
选择框
外观
选择框的外观经过权衡选择使用 LineRenderer 实现,将它的 Size 设置为5以容纳一个四边闭合的矩形线框,具体的点我们在脚本里计算。 给它一个白色的材质,随意设置一个你喜欢的颜色,并调整宽度到合适。 要注意的是将 ++Use World Space 取消选中++{.dot},这样绘制出来的框体就会跟随物体的位置。
现在我们来处理鼠标。
首先是左键按下,确定了一个点,随后在拖动中更新另一个点绘制线框,在鼠标抬起后确定了另一个点。
记录鼠标起点,通过判断 nullable 来确定鼠标是否已经按下。
Vector3? selection_start_mouse_position;
总是会用到的鼠标当前位置
Vector3 position = Camera.main.ScreenToWorldPoint(Input.mousePosition);
鼠标按下,激活选择框对象,记录位置,这里面的判断是为了避开上层的UI元素,你也不想点小地图就取消了当前单位的选择对吧。
if (Input.GetKeyDown(KeyCode.Mouse0))
{
if (EventSystem.current.IsPointerOverGameObject())
return;
selection_start_mouse_position = position;
selectionBox.SetActive(true);
}
鼠标拖动时,调整 LineRenderer 不必说,随后需要调整整个物体的中心位置,确保起始点不变。
if (Input.GetKey(KeyCode.Mouse0))
{
position.z = 100;
if (selection_start_mouse_position is Vector3 sp)
{
Vector3 size = position - sp;
size.z = 0;
var half_size = size / 2;
selectionBox.GetComponent<LineRenderer>().SetPositions(new Vector3[]
{
new(half_size.x, half_size.y),
new(half_size.x,-half_size.y),
new(-half_size.x,-half_size.y),
new(-half_size.x,half_size.y),
new(half_size.x, half_size.y)
});
selectionBox.transform.position = position - half_size;
}
}
在抬起鼠标时,重置大小并隐藏。
if (Input.GetKeyUp(KeyCode.Mouse0))
{
if (selection_start_mouse_position is Vector3 sp)
{
selectionBox.SetActive(false);
selectionBox.GetComponent<LineRenderer>().SetPositions(new Vector3[] { });
selection_start_mouse_position = null;
}
}
功能
一个很自然的想法是使用起始两点确定的矩形,判断单位是否在矩形内部,不过这样有个小问题,使用中心点判断时需要确保中心点在矩形的内部,不能轻易的实现符合视觉预期的效果,所以我选择了碰撞体。
在鼠标拖动时添加如下代码调整碰撞体大小,需要注意的是碰撞体的 size 参数需要保证顶点距离不能过小或为负。
if (Input.GetKey(KeyCode.Mouse0))
{
position.z = 100;
if (selection_start_mouse_position is Vector3 sp)
{
Vector3 size = position - sp;
size.z = 0;
var half_size = size / 2;
selectionBox.GetComponent<LineRenderer>().SetPositions(new Vector3[]
{
new(half_size.x, half_size.y),
new(half_size.x,-half_size.y),
new(-half_size.x,-half_size.y),
new(-half_size.x,half_size.y),
new(half_size.x, half_size.y)
});
selectionBox.GetComponent<BoxCollider2D>().size = new(Max(Abs(size.x), 0.01f), Max(Abs(size.y), 0.01f));
selectionBox.transform.position = position - half_size;
}
}
在鼠标抬起时,首先取消之前的选中,然后通过OverlapCollider
取得重叠的碰撞体,并选中这些碰撞体对应的单位。
if (Input.GetKeyUp(KeyCode.Mouse0))
{
if (selection_start_mouse_position is Vector3 sp)
{
foreach (var ship in mainPlay.selectedShips)
{
ship.GetComponent<Warship>().IsSelected = false;
}
mainPlay.selectedShips.Clear();
List<Collider2D> shipColliders = new();
selectionBox.GetComponent<BoxCollider2D>().OverlapCollider(
new ContactFilter2D {
useLayerMask = true,
layerMask = LayerMask.GetMask("Unit")
}, shipColliders);
mainPlay.selectedShips = shipColliders.Select(c => c.gameObject).ToList();
foreach (var ship in mainPlay.selectedShips)
{
ship.GetComponent<Warship>().IsSelected = true;
}
selectionBox.SetActive(false);
selectionBox.GetComponent<LineRenderer>().SetPositions(new Vector3[] { });
selection_start_mouse_position = null;
}
}
:::info 此处使用了 ContactFilter 来排除其他层中的物体。 目前并未进行单位阵营归属的判断。 :::
至此,框选功能完成。
小地图:续,更多的交互
在上一篇中只是实现了小地图的显示功能,下面开始完善。
显示当前镜头范围
也就是小地图上的白框,我们可以延续之前显示单位缩略图的思路,给 Main Camera 添加一个 minimap 层的子物体。
同样使用 LineRenderer 来绘制。 只需要在游戏初始化的时候设置一下。
void Start()
{
// 获取主摄像头范围
half_cam_size = (Camera.main.ScreenToWorldPoint(new Vector3(Screen.width, Screen.height)) - Camera.main.ScreenToWorldPoint(Vector3.zero))/2;
GameObject.Find("Camera Range").GetComponent<LineRenderer>().SetPositions(
new Vector3[] {
new(-half_cam_size.x, half_cam_size.y),
new(half_cam_size.x, half_cam_size.y),
new(half_cam_size.x, -half_cam_size.y),
new(-half_cam_size.x, -half_cam_size.y),
new(-half_cam_size.x, half_cam_size.y)
});
}
点击小地图移动镜头
由于 Input.GetXX
会作用于全局,为了避免不必要的判断,作为 UI 组件可以使用 IPointerDownHandler
系列接口获取点击事件。
public void OnPointerDown(PointerEventData eventData)
{
Vector2 ratio = (eventData.position - Vector2.one * 100) / 200;
Camera.main.transform.position = new(ratio.x * mainPlay.MapSize.x, ratio.y * mainPlay.MapSize.y, -10);
}
小地图的大小目前设置为 200,故鼠标事件的位置需要减去一半大小后再除以 200 得到点击位置相对于全图的比例。 注意不要改动摄像机高度。
更进一步。
bool map_operation = false;
// Update is called once per frame
void Update()
{
if (map_operation && Input.GetKey(KeyCode.Mouse0))
{
Vector3 p = Input.mousePosition;
p.x = Mathf.Clamp(p.x, 0, 200);
p.y = Mathf.Clamp(p.y, 0, 200);
Vector2 ratio = (p - Vector3.one * 100) / 200;
Camera.main.transform.position = new(ratio.x * mainPlay.MapSize.x, ratio.y * mainPlay.MapSize.y, -10);
}
}
public void OnPointerDown(PointerEventData eventData)
{
map_operation = true;
}
public void OnPointerUp(PointerEventData eventData)
{
map_operation= false;
}