将人物放置于世界坐标的(0,0.1,0),初始道路放置于人物脚下,坐标设置为(0,0,0),水面初始位置设置为(0,-2,-100),可以自行调整,道路与人物在y轴上不要 重合即可,当然这些操作也可以放在Awake函数中动态进行,但直接摆放显然是最快捷的方法。
这时候点击运行游戏大概率什么也看不到,我们需要将摄像机放置于人物面前,按照如下设置其transform组件的属性。
这时点击运行会看到如下画面:
如果上一篇的动画状态机设置正确的话,人物应该会处于待机动画,而需要这个动画循环的话,只需要将该动画的Loop Time和其子属性Loop Pose勾选即可,如下。
那么,到现在场景的摆放基本完成,ui方面会在下篇说明。
一款简单的跑酷游戏的人物需要有前进,跳跃,下滑和左右移动的动作,而人物在前进的同时需要摄像头跟随,这里就需要脚本实现了。
/*CameraPosition.cs,挂载于main camera*/
public GameObject character;
public float beh=4;//相机距离
public float ups=7;//相机高度
void LateUpdate()
{
if (start)
{
transform.position = character.transform.position - Vector3.forward * beh + Vector3.up * ups;
transform.LookAt(character.transform);
}
}
正如第一篇所说,相机的跟随需要 在人物位置改变之后,因此在LateUpdate中进行。
人物进入游戏后设置为奔跑状态。
public Animator playerAnimator;
public int framerate=30;
CharacterController ctrl;
bool isjumping = false;
bool issliding = false;
bool ismoving = false;
bool isleft = false;
bool isright = false;
public float speed = 5f;
public GameObject hard;//难度
public Vector2 screenaixY = new Vector2(0, 1);
public Vector2 screenaixX = new Vector2(1, 0);
Vector2 startpos;
Vector2 endpos;
bool isinput = false;
float angle;
void Start()
{
Transform pos;
pos=GetComponent<Transform>();
pos.position = new Vector3(0, 0.1f, 0);
ctrl = GetComponent<CharacterController>();
playerAnimator = GetComponent<Animator>();
playerAnimator.SetBool("MOVE", true);
}
接下来在Update函数中完成其他动作及其音效的触发。
#if UNITY_STANDALONE_WIN//如果是windows平台
if (Input.GetKeyDown(KeyCode.UpArrow) && (isjumping == false) && (issliding == false))
{
GetComponent<AudioSource>().clip = Resources.Load<AudioClip>("jump");
GetComponent<AudioSource>().PlayOneShot(GetComponent<AudioSource>().clip);
isjumping = true;
playerAnimator.SetBool("ISJUMP", true);
}
if (Input.GetKeyDown(KeyCode.DownArrow) && (isjumping == false) && (issliding == false))
{
GetComponent<AudioSource>().clip = Resources.Load<AudioClip>("slide");
GetComponent<AudioSource>().PlayOneShot(GetComponent<AudioSource>().clip);
issliding = true;
playerAnimator.SetBool("ISSLIDE", true);
}
if (Input.GetKeyDown(KeyCode.LeftArrow) && (!isright) && (!isleft))
{
isleft = true;
StartCoroutine(turnleft());
}
/*while(transform.position.x >= -f)
transform.position += new Vector3(-1, 0, 0)*Time.deltaTime*5;*/
if (Input.GetKeyDown(KeyCode.RightArrow) && (!isleft) && (!isright))
{
isright = true;
StartCoroutine(turnright());//协程的使用
}
#endif
#if UNITY_ANDROID//如果是Android平台
if(Input .touchCount==1)
{
if (Input.touches[0].phase == TouchPhase.Began)
startpos = Input.touches[0].position;
if(Input.touches[0].phase==TouchPhase.Ended&&Input .touches[0].phase!=TouchPhase.Canceled)
{
endpos = Input.touches[0].position;
isinput = true;
}
}
if (isinput)
{
Vector2 nowdir = endpos - startpos;
float cosvx = Vector3.Dot(nowdir, screenaixX) / nowdir.magnitude * screenaixX.magnitude;
float cosvy = Vector3.Dot(nowdir, screenaixY) / nowdir.magnitude * screenaixY.magnitude;
angle = Mathf.Acos(cosvy) * Mathf.Rad2Deg;
if (cosvx < 0)
{
if (angle >= 0 && angle <= 45)
{
GetComponent<AudioSource>().clip = Resources.Load<AudioClip>("jump");
GetComponent<AudioSource>().PlayOneShot(GetComponent<AudioSource>().clip);
isjumping = true;
playerAnimator.SetBool("ISJUMP", true);
}
else if (angle > 45 && angle < 135)
{
isleft = true;
StartCoroutine(turnleft());
}
else if (angle >= 135 && angle < 180)
{
GetComponent<AudioSource>().clip = Resources.Load<AudioClip>("slide");
GetComponent<AudioSource>().PlayOneShot(GetComponent<AudioSource>().clip);
issliding = true;
playerAnimator.SetBool("ISSLIDE", true);
}
}
else
{
if (angle >= 0 && angle <= 45)
{
GetComponent<AudioSource>().clip = Resources.Load<AudioClip>("jump");
GetComponent<AudioSource>().PlayOneShot(GetComponent<AudioSource>().clip);
isjumping = true;
playerAnimator.SetBool("ISJUMP", true);
}
else if (angle > 45 && angle < 135)
{
isright = true;
StartCoroutine(turnright());
}
else if (angle >= 135 && angle < 180)
{
GetComponent<AudioSource>().clip = Resources.Load<AudioClip>("slide");
GetComponent<AudioSource>().PlayOneShot(GetComponent<AudioSource>().clip);
issliding = true;
playerAnimator.SetBool("ISSLIDE", true);
}
}
isinput = false;
}
#endif
这里补充一下安卓部分的控制,原理是将触屏操作转化为向量,与x轴和y轴取余弦值,区分出向量所在象限,并同时通过对结果取反三角函数得出角度,如下。
上述调用的左移右移动作子函数,在update外实现。
IEnumerator turnleft()
{
float dis = 0;
while ((transform.position.x >= -2.1f) && (dis <= 2.1) && isleft)//保证移动的单位化
{
transform.position += new Vector3(-1, 0, 0) * Time.deltaTime * 5;
dis += Time.deltaTime * 5;
yield return null;
}
isleft = false;
yield break;
}
IEnumerator turnright()
{
float dis = 0;
while ((transform.position.x <= 2.1f) && isright && (dis <= 2.1))
{
transform.position += new Vector3(1, 0, 0) * Time.deltaTime * 5;
dis += Time.deltaTime * 5;
yield return null;
}
isright = false;
yield break;
}
这样就完成了原地的动作释放,这样释放的跳跃和下滑动作是一直进行的,原因是动画状态机的状态一直持续,未能跳转回正常奔跑状态。
正确操作是需要在动画播放完毕后即使跳转,即在Update中每帧检测动画状态:
jumpend();
slideend();
在Update外的子函数:
public void jumpend()
{
AnimatorStateInfo info = playerAnimator.GetCurrentAnimatorStateInfo(0);
if (info.IsName("JUMP") && (info.normalizedTime >= 1.0f))
{
playerAnimator.SetBool("ISJUMP", false);
isjumping = false;
}
}
public void slideend()
{
AnimatorStateInfo info = playerAnimator.GetCurrentAnimatorStateInfo(0);
if (info.IsName("SLIDE") && (info.normalizedTime >= 1.0f))
{
playerAnimator.SetBool("ISSLIDE", false);
issliding = false;
}
}
人物前进在Update中使用CharacterController提供的move函数实现。
if (ismoving)
{
playerAnimator.SetFloat("MOVESPEED", 2.8f);
Moveincrease = transform.forward * speed * Time.deltaTime;
if (Time.timeScale != 0)
score += Moveincrease.z;
ctrl.Move(Moveincrease);
speed += 0.005f * (float)System.Math.Pow((Time.deltaTime), 1 / 3);
}
这样就完成了人物动作的完整实现。
点击运行,我们就可以对人物进行操作了。
接下来,我们来动态生成道路。
跑酷游戏以人物的位置为参数生成道路等,于是我们选择在同一脚本的Update中实现该功能。
/*定义*/
public GameObject bridge;
public GameObject wave;
public GameObject gold;
public GameObject[] hurdle;
int count = 0;
if (transform.position.z >= 7.5f * count)
{
float z = 72f + count * lenth;
float[] hurdle_x = new float[3] { -width / 2, 0, width / 2 };
float hurdle_y = 0f;
float z1 = z - 0.75f * lenth;
float z2 = z - 0.5f * lenth;
float z3 = z - 0.25f * lenth;
float goldpos;
float[] hurdle_z = new float[3] { z1, z2, z3 };
Instantiate(bridge, new Vector3(0, 0, z), Quaternion.identity);//生成道路
count++;
for (int i = 0; i <= maxhurdle; i++)
Instantiate(hurdle[Random.Range(0, 3)], new Vector3(hurdle_x[Random.Range(0, 3)], hurdle_y, hurdle_z[i % 3]), Quaternion.identity);//生成障碍物
goldpos = hurdle_x[Random.Range(0, 3)];
for (int j = 0; j <= 5; j++)
Instantiate(gold, new Vector3(goldpos, hurdle_y+1, hurdle_z[0] + j * 3), Quaternion.Euler(180,0,180));
z = 72f + count * lenth;
Instantiate(bridge, new Vector3(0, 0, z), Quaternion.identity);
count++;
z1 = z - 0.75f * lenth;
z2 = z - 0.5f * lenth;
z3 = z - 0.25f * lenth;
hurdle_z = new float[3] { z1, z2, z3 };
for (int i = 0; i <= maxhurdle; i++)
Instantiate(hurdle[Random.Range(0, 3)], new Vector3(hurdle_x[Random.Range(0, 3)], hurdle_y, hurdle_z[i % 3]), Quaternion.identity);
z = 72f + count * lenth;
Instantiate(bridge, new Vector3(0, 0, z), Quaternion.identity);
count++;
z1 = z - 0.75f * lenth;
z2 = z - 0.5f * lenth;
z3 = z - 0.25f * lenth;
hurdle_z = new float[3] { z1, z2, z3 };
for (int i = 0; i <= maxhurdle; i++)
Instantiate(hurdle[Random.Range(0, 3)], new Vector3(hurdle_x[Random.Range(0, 3)], hurdle_y, hurdle_z[i % 3]), Quaternion.identity);
}
if (transform.position.z >= 1000)
{
wave.transform.position += new Vector3(0, 0, transform.position.z);
}
}
此时点击运行,会跟随人物生成道路和障碍物,但是人物无法判定碰撞障碍物。
CharacterController提供一个碰撞检测函数OnControllerColliderHit,这个函数会传递一个ControllerColliderHit类的对象作为参数,给出碰撞对象的信息,非常方便。
void OnControllerColliderHit(ControllerColliderHit hit)
{
if (isjumping && ((hit.collider.name == "Hurdle01(Clone)") || (hit.collider.name == "Hurdle02(Clone)")))
{
score +=10;
Destroy(hit.collider);
}
else if (issliding && ((hit.collider.name == "Hurdle03(Clone)") || (hit.collider.name == "Hurdle01(Clone)")))
{
score +=10;
Destroy(hit.collider);
}
else if(hit.collider.name=="Goldfish(Clone)")
{
score +=4;
Transform tf=hit.collider.GetComponent<Transform>();
StartCoroutine(fishdisappear(tf));
}
else if(hit.collider.name=="Ground")
{ }
else
{
playerAnimator.SetBool("ISDEATH", true);
gameover = true;
}
}
利用条件处理各种碰撞,同时将其销毁,减少内存消耗。这样处理的好处就是不需要考虑人物体积和碰撞体体积等复杂处理,只需要在碰撞时检测是否做出相应动作即可。
上述函数的子函数:
IEnumerator fishdisappear(Transform fishtran)
{
//yield return new WaitForSeconds(0.01f);
GetComponent<AudioSource>().clip = Resources.Load<AudioClip>("eat");
GetComponent<AudioSource>().PlayOneShot(GetComponent<AudioSource>().clip);
fishtran.gameObject.GetComponent<BoxCollider>().enabled = false;
while (fishtran.position.y<=10)
{
fishtran.position += new Vector3(0, 1f, 0);
yield return null;
}
if(fishtran.gameObject)
Destroy(fishtran.gameObject);
yield break;
}
Update中的死亡判定:
if (gameover == true)
{
StartCoroutine(over());
GetComponent<AudioSource>().clip = Resources.Load<AudioClip>("death");
if (GetComponent<AudioSource>().enabled)
if ((!GetComponent<AudioSource>().isPlaying) && (inum++ == 0))
GetComponent<AudioSource>().PlayOneShot(GetComponent<AudioSource>().clip);
}
死亡处理子函数:
IEnumerator over()
{
AnimatorStateInfo info = playerAnimator.GetCurrentAnimatorStateInfo(0);
// Debug.Log("normal" + info.normalizedTime);
while (true)
{
yield return new WaitForSeconds(0.5f);
if (info.IsName("DEATH"))
{
playerAnimator.SetBool("ISDEATH", false);
//gameover = false;
//Debug.Log("timescale");
//yield return new WaitForSeconds ()
GetComponent<AudioSource>().enabled = false;
Time.timeScale = 0;
}
}
}
Update中的其他一些处理(由于动画资源并不完全合适,主要用于调整):
int inum = 0;
AnimatorStateInfo info = playerAnimator.GetCurrentAnimatorStateInfo(0);
if (info.normalizedTime >= 1.0f) waitaction = true;
if (info.IsName("RUN") && waitaction) ismoving = true;
if (info.IsName("RUN")) playerAnimator.speed = 1.5f * Time.timeScale;
if (info.IsName("JUMP")) playerAnimator.speed = 5f * Time.timeScale;
if (info.IsName("SLIDE")) playerAnimator.speed = Time.timeScale;