每周小结004

视角移动和缩放

假如你的游戏场景是在XZ平面的,而你的相机是俯视角的,你希望玩家可以拖动屏幕来移动视角,双指交互可以缩放视角。针对这个需求,在FGUI里,包含用户输入的组件:SwipeGesture和PinchGesture。拖动相机分成两个阶段:第一阶段,手指按下并拖动,此时要求场景完全跟随手指移动(跟手);第二阶段,当手指松开后,根据松开前delta阈值,低于阈值则不进行惯性操作,否则需要根据松开前的速度进行惯性制动位移。

正交相机

正交相机的orthographicSize代表相机高度的一半对应的世界距离,将它乘以2再除以屏幕高度,就可以得到单位像素对应的世界距离。FGUI屏幕delta可以转化为unity屏幕像素delta,最终对应世界空间的delta。
通过这种映射关系,移动视角的第一阶段,可以算出世界空间的delta;移动视角的第二阶段,可以算出世界空间的velocity。

透视相机

透视相机的屏幕像素对应距离不再是均匀的,所以需要通过射线算距离,由于我们场景是XZ平面,所以不需要真的做射线检测,只需要射线方向,根据相机距离,就可以算出射线与XZ平面的距离,然后直接计算两点射线与XZ平面交点的delta就可以了。移动视角的第一阶段,这种方式可以完美适配;但是对于第二阶段,我们简单处理,采用的是近似的方式,即选取相机中点射线,结合相机的fieldOfView,算出相机近似高度所对应的世界距离,然后将FGUI逻辑像素速度转换为近似的世界空间速度。

有几点需要注意

  • 相机是有俯仰角的,比如30°的俯视(x轴旋转30),玩家水平拖动屏幕时,相机进行本地坐标空间的左右移动,此时映射到世界空间,是1:1的。但是当玩家竖直拖动屏幕时,相机不再是本地空间的上下移动(因为我们希望相机的移动平面也是XZ平面,只不过是高于游戏场景地面的XZ平面),因此我们相机原本在本地空间上下移动的Δd需要“反向投影”到XZ平面,因此需要将位移除以$sin(θ)$还原到XZ平面的真实位移。
  • FGUI的Gesture里的delta是fgui的逻辑像素变化量(比如设定的1920x1080),需要将他乘以GRoot.contentScaleFactor还原到当前屏幕像素的变化量,此时原点还是FGUI的左上角,需要被Screen.height减去对应y值,转换为unity的屏幕原点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
using Cysharp.Threading.Tasks;
using FairyGUI;
using Framework.Runtime;
using GameLogic.Battle.Core.Event;
using UnityEngine;
using Logger = Framework.Base.Logger;

namespace GameLogic.Home
{
public class CameraInteractionByGesture
{
private const float _GroundHeight = 0;
private const float _SwipeBrakeThreshold = 10 * 10;

private readonly SwipeGesture _swipeGesture;
private readonly PinchGesture _pinchGesture;

private Vector2 _screenDelta; // FGui SwipeGesture在onEnd无法正确设置delta,所以外部保存下
private Camera _idleCamera;

public CameraInteractionByGesture(GObject host, Camera camera)
{
_swipeGesture = new SwipeGesture(host) { snapping = false };
_pinchGesture = new PinchGesture(host);
_idleCamera = camera;
BindPinch();
BindSwipe();
}

private void BindPinch()
{
_pinchGesture.onAction.Set(() => new CameraPinchEvent(-_pinchGesture.delta).Fire());
}

private void BindSwipe()
{
if (_idleCamera!.orthographic)
BindSwipeForOrtho();
else
BindSwipeForPerspective();
}

# region 正交相机
private void BindSwipeForOrtho()
{
if (_idleCamera.IsNullPtr()) return;
_swipeGesture.onMove.Set(() =>
{
if (_idleCamera.IsNullPtr()) return;
#if UNITY_EDITOR
if (_idleCamera.orthographic == false)
{
BindSwipeForPerspective();
return;
}
#endif
_screenDelta = _swipeGesture.delta * GRoot.contentScaleFactor;
var worldPerPixel = _idleCamera.orthographicSize * 2f / Screen.height;
var backProjection = 1f / Mathf.Sin(_idleCamera.transform.eulerAngles.x * Mathf.Deg2Rad);
var deltaXZ = _screenDelta * worldPerPixel;
var deltaXYZ = new Vector3(-deltaXZ.x, 0, deltaXZ.y * backProjection);
var worldDelta = Quaternion.Euler(0, _idleCamera.transform.eulerAngles.y, 0) * deltaXYZ;
new CameraSwipeEvent(new Vector2(worldDelta.x, worldDelta.z), Vector2.zero, CameraSwipeType.Moving).Fire();
});
_swipeGesture.onEnd.Set(() =>
{
if (_idleCamera.IsNullPtr()) return;
if (_screenDelta.sqrMagnitude < _SwipeBrakeThreshold)
{
new CameraSwipeEvent(Vector2.zero, Vector2.zero, CameraSwipeType.Braking).Fire();
return;
}
var screenVelocity = _swipeGesture.velocity * GRoot.contentScaleFactor;
var worldPerPixel = _idleCamera.orthographicSize * 2f / Screen.height;
var backProjection = 1f / Mathf.Sin(_idleCamera.transform.eulerAngles.x * Mathf.Deg2Rad);
var velocityXZ = screenVelocity * worldPerPixel;
var velocityXYZ = new Vector3(-velocityXZ.x, 0, velocityXZ.y * backProjection);
var worldVelocity = Quaternion.Euler(0, _idleCamera.transform.eulerAngles.y, 0) * velocityXYZ;
new CameraSwipeEvent(Vector2.zero, new Vector2(worldVelocity.x, worldVelocity.z), CameraSwipeType.Braking).Fire();
});
}
# endregion

#region 透视相机
private void BindSwipeForPerspective()
{
if (_idleCamera.IsNullPtr()) return;
_swipeGesture.onMove.Set(context =>
{
#if UNITY_EDITOR
if (_idleCamera.orthographic)
{
BindSwipeForOrtho();
return;
}
#endif
if (_idleCamera.IsNullPtr()) return;
_screenDelta = _swipeGesture.delta * GRoot.contentScaleFactor;

var currentScreenPos = context.inputEvent.position;
var prevScreenPos = currentScreenPos - _screenDelta;
var currentUnityScreenPos = new Vector2(currentScreenPos.x, Screen.height - currentScreenPos.y);
var prevUnityScreenPos = new Vector2(prevScreenPos.x, Screen.height - prevScreenPos.y);

var worldCurrent = ScreenToWorldPoint(_idleCamera, currentUnityScreenPos);
var worldPrev = ScreenToWorldPoint(_idleCamera, prevUnityScreenPos);
var realWorldDelta = worldPrev - worldCurrent;
new CameraSwipeEvent(new Vector2(realWorldDelta.x, realWorldDelta.z), Vector2.zero, CameraSwipeType.Moving).Fire();
});
_swipeGesture.onEnd.Set(() =>
{
if (_idleCamera.IsNullPtr()) return;
if (_screenDelta.sqrMagnitude < _SwipeBrakeThreshold)
{
new CameraSwipeEvent(Vector2.zero, Vector2.zero, CameraSwipeType.Braking).Fire();
return;
}
var screenVelocity = _swipeGesture.velocity * GRoot.contentScaleFactor;
float distance = CalculateFocusDistance(_idleCamera.transform.rotation * Vector3.forward, _idleCamera.transform.position.y);
var viewHeight = Mathf.Tan( Mathf.Deg2Rad * _idleCamera.fieldOfView / 2) * distance * 2f;
// 透视相机单位像素距离不是均匀的,用中点近似
var worldPerPixel = viewHeight / Screen.height;
var backProjection = 1f / Mathf.Sin(_idleCamera.transform.eulerAngles.x * Mathf.Deg2Rad);
var velocityXZ = screenVelocity * worldPerPixel;
var velocityXYZ = new Vector3(-velocityXZ.x, 0, velocityXZ.y * backProjection);
var worldVelocity = Quaternion.Euler(0, _idleCamera.transform.eulerAngles.y, 0) * velocityXYZ;
new CameraSwipeEvent(Vector2.zero, new Vector2(worldVelocity.x, worldVelocity.z), CameraSwipeType.Braking).Fire();
});
}

private static Vector3 ScreenToWorldPoint(Camera camera, Vector2 screenPos)
{
Ray ray = camera.ScreenPointToRay(screenPos);
var dir = ray.direction;
var depth = CalculateFocusDistance(dir, camera.transform.position.y);
return ray.GetPoint(depth);
}

private static float CalculateFocusDistance(Vector3 dir, float cameraY)
{
Vector3 forward = dir.normalized;
if (Mathf.Abs(forward.y) < 0.001f) return 10;
float heightDiff = _GroundHeight - cameraY;
float distance = heightDiff / forward.y;
return distance;
}
#endregion
}
}