基于PF规则的CRPG制作尝试(四)移动预设线
上一步完成了角色的移动控制,包括通常状态(IDLE)时的移动和战斗状态(SWORDIDLE)状态时的移动。但其实这其中包括一个问题,也就是说在我心中的回合制CRPG,战斗状态时是不应该能做到自由移动的。在战斗状态中的移动应该是以:点击位置——出现移动预设线——确认——移动,这种方式进行移动的。由于PF规则,其中还应该包括移动距离的显示,PF规则中移动的距离是根据格子计算的,这是由于地图样式导致的,而CRPG毕竟是通过电脑进行运算,直接计算距离即可。
先说一下思路:
1.进入战斗状态。
2.点击鼠标右键进行移动位置预设,在这个过程中是可以通过再次点击右键更改预设线的位置的。
3.点击鼠标左键确认位置,进行移动。
4.调整预设线显示状态
5.计算移动距离并显示
EX.改变预设线样式
这里与要注意的是,之前我是把通常状态和战斗状态的移动用一个move函数直接使用的,区别只有播放的动画不同,但是由于预设线的移动是需要进行左键右键的交互点击的,并且不能像普通状态一样即点即走,我个人是个小白,对优秀的代码结构也不甚了解,就选择最笨的办法将移动代码进行修改放入按移动预设线移动部分(新建的移动预设状态)的代码中。接下来是具体操作部分。
1.进入战斗状态
没什么好说的,先设置一个新的state命名为MOVEPRE(移动预设),声明一个新的战斗时跑步速度,然后设置点击U键进入战斗
if (Input.GetKeyDown(KeyCode.U))
{
if (state == STATE.SWORDIDLE)
{
state = STATE.MOVEPRE;
}
//再点击U键回到普通战斗状态
else if (state == STATE.MOVEPRE)
{
state = STATE.SWORDIDLE;
}
}
2.点击鼠标右键进行移动位置预设
此部分我是通过声明新函数DrawPreviewLine实现的
if (Input.GetMouseButtonDown(1))
{
if (state == STATE.MOVEPRE)
{
// anim.SetBool("movewithsword", true);
DrawPreviewLine();
}
}
点击鼠标右键,如果此时是MOVEPRE状态的话,就会调用DrawPreviewLine函数,DrawPreviewLine主要由以下功能部分组成:
①确定鼠标点击位置的世界坐标
本部分与上一节一致,此处不再赘述
②根据鼠标点击位置和模型位置两点确定一条线段即为移动预设线
这里我们使用LineRenderer方法
首先在AC中public一个 LineRenderer 命名为movepreline,然后新建一个gameobject命名为line,在line上添加一个LineRenderer 组件,调整宽度和材质到自己想要的效果。然后设置起点和终点。
if (Physics.Raycast(ray, out hitInfo))
{
//设置LineRenderer顶点数
movepreline.positionCount = 2;
movepreline.SetPosition(0, AcotorTrans.position);
movepreline.SetPosition(1, hitInfo.point);
}
3.点击鼠标左键确认位置,进行移动
这部分和之前的区别不大,需要注意的点有:
1.我设置点击左键进行确认,将一个bool类型的变量moveconfirm由初始的false改为true,在该变量为true的条件下角色方能移动。移动结束后(PreEndPoints内点的数量为0时)变回false。
2.在预设终点PreEndPoints(与正常移动的EndPoints进行区分)内点的数量大于零的时候播放移动动画并进行移动。
具体代码:
if (state == STATE.MOVEPRE)
{
if (Input.GetMouseButton(0))
{
moveconfirm = true;
}
if (moveconfirm == true)
{
if (PreEndPoints.Count > 0)
{
anim.SetBool("movewithsword", true);
print("检测到终点数组非空");
print("终点坐标是:" + PreEndPoints[0]);//经验证,终点坐标识别的没问题
Vector3 v = PreEndPoints[0] - AcotorTrans.position;
var dot = Vector3.Dot(v, AcotorTrans.right);
Vector3 next = v.normalized * runSpeed * Time.deltaTime;
//计算转头角度
float angle = Vector3.Angle(v, AcotorTrans.forward);
if (Vector3.SqrMagnitude(v) > 1f)
{
float minAngle = Mathf.Min(angle, rotatePower * Time.deltaTime);
//点乘
if (angle > 1f)
{
//transform.Rotate(Vector3.Cross(tank.forward, v.normalized), minAngle);
if (dot > 0)
{
AcotorTrans.Rotate(new Vector3(0, minAngle, 0));
}
else
{
AcotorTrans.Rotate(new Vector3(0, -minAngle, 0));
}
}
else
{
AcotorTrans.LookAt(PreEndPoints[0]);
AcotorTrans.position += next;
print("走完了一小步");
}
}
else
{
//清空PreEndPoints
PreEndPoints.RemoveAt(0);
}
}
else
{
anim.SetBool("movewithsword", false);
moveconfirm = false;
}
}
}
4.调整预设线显示状态
我认为预设线应该在预设移动状态第一次点击鼠标右键,即第一次选择预设移动位置时出现,在移动开始时停止。具体的实现方法我选择使用第二节的改变line的layer的方法,具体不再赘述。
5.计算移动距离并显示
计算距离并不困难,使用Vector2.Distance方法即可(因为y坐标是相同的),但是显示距离这一步出现了一些问题,首先我认为比较好的表现形式是在预设移动线的中点显示距离,但是实际上由于预设线的方向是自由的,在某些方向上会出现无法看清距离显示的情况。就解决方法而言我个人想到的是将这个距离的显示方向设为永远朝向摄像机,但摄像机部分暂时应该还接触不到,就暂且搁置,改为在产生预设线时print出距离显示。
遇到的问题
1.确定预设线时多次点击会导致角色按照点击的顺序进行折线移动
解决方法
经过代码结构分析,我发现这是由于在构建预设线时,每一次点击的点都会被记录下来,所以我选择在每次点击产生预设线时PreEndPoints.Clear(),即将PreEndPoints中的点清空,这样就能保证移动的目的地是最后一次移动预设确定的终点。
2.在按预设线移动时点击新的地点会立刻离开原方向向新的目的地移动
解决方法
这是由于在移动过程中点击新的地点会覆盖原目的地所导致的,我添加了一个bool变量ismoving,初始值设为false,在点击鼠标左键开始移动后将ismoving的值改为true,令当且仅当ismoving的值为false时,才可以记录新的终点位置到PreEndPoints中,解决了问题。
本节的完整代码如下:
public enum STATE
{
IDLE,
SWORDIDLE,
MOVEPRE,
}
private bool moveconfirm = false;
private bool ismoving = false;
public STATE state;
Transform AcotorTrans;
List<Vector3> PreEndPoints;
public LineRenderer movepreline;
// moveSpeed 是一个浮点数, 为预设的角色移动速度
public static float runSpeed = 2.5f;
// rotatePower 是一个浮点数, 为与角色转向速度相关的预设值
public static float rotatePower = 600.0f;
void Start()
{
AcotorTrans = GetComponent<Transform>();
PreEndPoints= new List<Vector3>();
}
void Update()
{
if (Input.GetKeyDown(KeyCode.U))
{
if (state == STATE.SWORDIDLE)
{
state = STATE.MOVEPRE;
}
else if (state == STATE.MOVEPRE)
{
state = STATE.SWORDIDLE;
ChangeLayer(movepreline.transform, "line");
}
}
//if(state!=STATE.MOVEPRE&&movepreline.transform.gameObject.layer==line)
if (Input.GetMouseButtonDown(1))
{
if (state == STATE.IDLE)
{
anim.SetBool("movewithoutsword", true);
UpdateControl();
}
if (state == STATE.MOVEPRE)
{
// anim.SetBool("movewithsword", true);
DrawPreviewLine();
}
}
void DrawPreviewLine()
{
if (Input.GetMouseButton(1)&&ismoving==false)
{
ChangeLayer(movepreline.transform, "Delta");
Vector3 mousepostion = Input.mousePosition;
//定义从屏幕
Ray ray = Camera.main.ScreenPointToRay(mousepostion);
RaycastHit hitInfo;
//如果点击并有交点
if (Physics.Raycast(ray, out hitInfo))
{
PreEndPoints.Clear();
//设置LineRenderer顶点数
movepreline.positionCount = 2;
movepreline.SetPosition(0, AcotorTrans.position);
movepreline.SetPosition(1, hitInfo.point);
Vector3 v3 = hitInfo.point - AcotorTrans.position;
hitInfo.point += v3.normalized;
print(Vector2.Distance(new Vector2(AcotorTrans.transform.position.x, AcotorTrans.transform.position.z),
new Vector2(hitInfo.point.x, hitInfo.point.y)));
PreEndPoints.Add(hitInfo.point);
// displaydistance.GetComponent<Text>().text = "distance : " + Vector2.Distance(new Vector2(AcotorTrans.transform.position.x, AcotorTrans.transform.position.z),
//new Vector2(hitInfo.point.x, hitInfo.point.y));
}
}
}
if (state == STATE.MOVEPRE)
{
if (Input.GetMouseButton(0))
{
ismoving = true;
moveconfirm = true;
}
if (moveconfirm == true)
{
if (PreEndPoints.Count > 0)
{
ChangeLayer(movepreline.transform, "line");
anim.SetBool("movewithsword", true);
print("检测到终点数组非空");
print("终点坐标是:" + PreEndPoints[0]);//经验证,终点坐标识别的没问题
Vector3 v = PreEndPoints[0] - AcotorTrans.position;
var dot = Vector3.Dot(v, AcotorTrans.right);
Vector3 next = v.normalized * runSpeed * Time.deltaTime;
//计算转头角度
float angle = Vector3.Angle(v, AcotorTrans.forward);
if (Vector3.SqrMagnitude(v) > 1f)
{
float minAngle = Mathf.Min(angle, rotatePower * Time.deltaTime);
//点乘
if (angle > 1f)
{
//transform.Rotate(Vector3.Cross(tank.forward, v.normalized), minAngle);
if (dot > 0)
{
AcotorTrans.Rotate(new Vector3(0, minAngle, 0));
}
else
{
AcotorTrans.Rotate(new Vector3(0, -minAngle, 0));
}
}
else
{
AcotorTrans.LookAt(PreEndPoints[0]);
AcotorTrans.position += next;
print("走完了一小步");
}
}
else
{
//清空PreEndPoints
PreEndPoints.RemoveAt(0);
}
}
else
{
anim.SetBool("movewithsword", false);
moveconfirm = false;
ismoving = false;
}
}
}
void ChangeLayer(Transform trans, string targetLayer)
{
//遍历更改所有子物体layer
trans.gameObject.layer = LayerMask.NameToLayer(targetLayer);
foreach (Transform child in trans)
{
ChangeLayer(child, targetLayer);
}
}
具体效果如下: