3D游戏作业7:巡逻兵
思路
本次作业还是采用MVC结构进行设计。不同的是,这次作业引用了一些动画。需要自定义Animator Controller以及相应的算法。
另外,为了使运动看上去更加“丝滑”,这次项目所有物体都使用物理引擎
成果
代码及解析
Director.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Director : System.Object
{
public static ISceneController current;
public static ISceneController GetCurrentScene(){
return current;
}
}
public interface ISceneController{
void LoadResources();
void GameOver();
void GetScore();
}
public interface DataSource{
int GetStatus();
Vector3 MePosition();
}
Director类是导演,用于管理当前工作的场记,这里只有FirstController。
ISceneController是场景管理接口。由FirstController继承。里面有一些场景重要的函数。LoadResources()用于生成资源,GameOver结束游戏,GetScore增加得分。
DateSource是体现订阅与发布模式编程用的。FirstController产生的信息,通过DataSource接口,传递给所有的巡逻兵和主角。GetStatus是获取游戏状态,游戏结束后一切停止。MePosition()获得主角的位置,巡逻兵距离主角较近就会追逐主角。
FirstController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FirstController : MonoBehaviour,ISceneController,DataSource
{
public GameObject me;
public GameObject camera;
public GameObject wall;
public GameObject patrol;
public GameObject hwall;
public GameObject light;
public Texture2D clo;
public int patrolNum=17;
public int wallNum=34;
public int score=0;
public int status=1;
public GUIStyle f;
// Start is called before the first frame update
void Start()
{
Random.InitState((int)System.DateTime.Now.Ticks);
Director.current=this;
LoadResources();
f.fontSize=50;
f.normal.textColor=new Color(255,255,255);
f.alignment=TextAnchor.MiddleCenter;
}
// Update is called once per frame
void Update()
{
camera.transform.position=me.transform.position+ new Vector3(0,12,-12);
light.transform.position=me.transform.position+new Vector3(0,5,-5);
}
void OnGUI(){
if (GUI.Button(new Rect(Screen.width-100,0,100,100),clo)){
Application.Quit();
}
GUI.Label(new Rect(0,Screen.height-100,Screen.width,100),"Score : "+score.ToString(),f);
if (status==0){
GUI.Label(new Rect(Screen.width/2,Screen.height/2,200,100),"Game Over !",f);
}
}
public void LoadResources(){
Instantiate(Resources.Load<GameObject>("Terrain"),new Vector3(150,-0.5f,150),Quaternion.identity).name="terrain";
me=Instantiate(Resources.Load<GameObject>("me"),new Vector3(150,1f,150),Quaternion.identity);
me.name="me";
me.AddComponent(typeof(MeController));
camera=this.gameObject;
wall=Resources.Load<GameObject>("wall");
light=Instantiate(Resources.Load<GameObject>("light"),me.transform.position,Quaternion.identity);
clo=Resources.Load<Texture2D>("15");
for(int i=1;i<=101;++i){
Instantiate(wall,new Vector3(99,1,99+i-1),Quaternion.identity).name="bianyuan";
Instantiate(wall,new Vector3(99+i-1,1,200),Quaternion.identity).name="bianyuan";
Instantiate(wall,new Vector3(200,1,200-i+1),Quaternion.identity).name="bianyuan";
Instantiate(wall,new Vector3(200-i+1,1,99),Quaternion.identity).name="bianyuan";
}
patrol=Resources.Load<GameObject>("patrol");
float[] x=new float[patrolNum+1];
float[] z=new float[patrolNum+1];
x[0]=150;z[0]=150;
for (int i=1;i<=patrolNum;++i){
while(true){
x[i]=Random.Range(105f,194f);
z[i]=Random.Range(105f,194f);
bool can=true;
for (int j=0;j<i;++j){
if ( (x[i]-x[j])*(x[i]-x[j]) + (z[i]-z[j])*(z[i]-z[j]) <=3 ){
can=false;
break;
}
}
if (can==true){
GameObject t=Instantiate(patrol,new Vector3(x[i],1f,z[i]),Quaternion.identity);
t.name="patrol";
t.AddComponent(typeof(PatrolController));
break;
}
}
}
hwall=Resources.Load<GameObject>("h");
for (int i=1;i<=wallNum;++i){
float xx=Random.Range(101f,198f);
float zz=Random.Range(101f,198f);
GameObject t=Instantiate(hwall,new Vector3(xx,1,zz),Quaternion.identity);
t.name="wall";
if (Random.Range(0f,100f)<=50){
t.transform.LookAt(t.transform.position+new Vector3(-1,0,0));
}
}
}
public int GetStatus(){
return status;
}
public Vector3 MePosition(){
return me.transform.position;
}
public void GameOver(){
status=0;
}
public void GetScore(){
score++;
}
}
场记类,用于管理所有的对象,资源。并且记录关键信息,发出游戏结束信号。
在LoadResources()里,加载了所有的巡逻兵,还有主角,地板,墙壁,障碍物等,并把他们放在合适的位置上。为其添加相应的组件。
同时也负责摄像机的移动,和光源位置的切换。
ObjectController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MeController:MonoBehaviour{
public GameObject me;
private float speed=10f;
private float ac=7;
private DataSource ds;
private Animator ani;
public Vector3 dir;
public Vector3 v;
void Start(){
me=this.gameObject;
ds=Director.GetCurrentScene() as DataSource;
ani=me.GetComponent<Animator>();
dir=new Vector3(0,0,0);
}
void FixedUpdate(){
if (ds.GetStatus()==0) {
me.GetComponent<Rigidbody>().velocity=new Vector3(0,0,0);
return;
}
if (Input.GetKey(KeyCode.W)){
if (me.GetComponent<Rigidbody>().velocity.z<=speed)
me.GetComponent<Rigidbody>().AddForce(new Vector3(0,0,1)*ac);
ani.SetBool("moving",true);
}
else{
// if (me.GetComponent<Rigidbody>().velocity.z>0)
// me.GetComponent<Rigidbody>().velocity-=new Vector3(0,0,me.GetComponent<Rigidbody>().velocity.z);
}
//--------------------------------------------------------------------------------------------------
if (Input.GetKey(KeyCode.S)){
if (me.GetComponent<Rigidbody>().velocity.z>=0-speed)
me.GetComponent<Rigidbody>().AddForce(new Vector3(0,0,-1)*ac);
ani.SetBool("moving",true);
}
else{
// if (me.GetComponent<Rigidbody>().velocity.z<0)
// me.GetComponent<Rigidbody>().velocity-=new Vector3(0,0,me.GetComponent<Rigidbody>().velocity.z);
}
//--------------------------------------------------------------------------------------------------------
if (Input.GetKey(KeyCode.A)){
if (me.GetComponent<Rigidbody>().velocity.x>=0-speed)
me.GetComponent<Rigidbody>().AddForce(new Vector3(-1,0,0)*ac);
ani.SetBool("moving",true);
}
else{
// if (me.GetComponent<Rigidbody>().velocity.x<0)
// me.GetComponent<Rigidbody>().velocity-=new Vector3(me.GetComponent<Rigidbody>().velocity.x,0,0);
}
//-------------------------------------------------------------------------------------------------------
if (Input.GetKey(KeyCode.D)){
if (me.GetComponent<Rigidbody>().velocity.x<=speed)
me.GetComponent<Rigidbody>().AddForce(new Vector3(1,0,0)*ac);
ani.SetBool("moving",true);
}
else{
// if (me.GetComponent<Rigidbody>().velocity.x>0)
// me.GetComponent<Rigidbody>().velocity-=new Vector3(me.GetComponent<Rigidbody>().velocity.x,0,0);
}
//----------------------------------------------------------------------------------------------
}
void LateUpdate(){
Vector3 s=me.GetComponent<Rigidbody>().velocity;
if (new Vector3(s.x,0,s.z).magnitude<=5f){
if (!Input.GetKey(KeyCode.W) && !Input.GetKey(KeyCode.S) &&!Input.GetKey(KeyCode.A) &&!Input.GetKey(KeyCode.D) )
ani.SetBool("moving",false);
}
if ((s.x!=0 || s.z!=0) && s.magnitude>=0.1f){
dir=new Vector3(s.x,0,s.z);
dir=dir/dir.magnitude;
}
me.transform.LookAt(me.transform.position+dir-new Vector3(0,dir.y,0));
v=me.GetComponent<Rigidbody>().velocity;
}
}
public class PatrolController:MonoBehaviour{
private GameObject patrol;
private float scale=5;
private float speed=5;
private float ac=6;
private DataSource ds;
private ISceneController isc;
public float[][] route;
public int next;
public int follow;
void Start(){
patrol=this.gameObject;
route=CreateRoute();
next=0;
follow=0;
ds=Director.GetCurrentScene() as DataSource;
isc=Director.GetCurrentScene() as ISceneController;
}
void FixedUpdate(){
if (ds.GetStatus()==0){
patrol.GetComponent<Rigidbody>().velocity=new Vector3(0,0,0);
return;
}
Vector3 mp=ds.MePosition();
if ((mp-patrol.transform.position).magnitude<=10){
Move(mp);
follow=1;
}
else{
if (follow==1){
route=CreateRoute();
next=0;
follow=0;
isc.GetScore();
}
Vector3 tar=new Vector3(route[next][0],patrol.transform.position.y,route[next][1]);
Move(tar);
if ( (tar-patrol.transform.position).magnitude <=0.1f){
next++;
if (next>3) next=0;
}
}
}
private float lc=0;
void OnCollisionStay(Collision c){
if (c.collider.name=="terrain" || ds.GetStatus()==0) return;
if (c.collider.name=="wall"){
if (Time.time-lc>=1f){
if (follow==0) next++;
if (next>3) next=0;
lc=Time.time;
}
}
if (c.collider.name=="me"){
isc.GameOver();
}
}
public float[][] CreateRoute(){
float[][] ans;
ans=new float[4][];
for (int i=0;i<4;++i) ans[i]=new float[2];
float[] x=new float[4];
float[] z=new float[4];
x[0]=patrol.transform.position.x-scale; z[0]=patrol.transform.position.z-scale;
x[1]=patrol.transform.position.x-scale; z[1]=patrol.transform.position.z+scale;
x[2]=patrol.transform.position.x+scale; z[2]=patrol.transform.position.z-scale;
x[3]=patrol.transform.position.x+scale; z[3]=patrol.transform.position.z+scale;
for (int i=0;i<4;++i){
if (x[i]<100) x[i]=100;
if (x[i]>199) x[i]=199;
if (z[i]<100) z[i]=100;
if (z[i]>199) z[i]=199;
}
ans[0][0]=Random.Range(x[0],x[1]); ans[0][1]=Random.Range(z[0],z[1]);
ans[1][0]=Random.Range(x[1],x[3]); ans[1][1]=Random.Range(z[1],z[3]);
ans[2][0]=Random.Range(x[2],x[3]); ans[2][1]=Random.Range(z[2],z[3]);
ans[3][0]=Random.Range(x[0],x[2]); ans[3][1]=Random.Range(z[0],z[2]);
return ans;
}
public void Move(Vector3 target){
target=new Vector3(target.x,patrol.transform.position.y,target.z);
Vector3 dir=target-patrol.transform.position;
float s=patrol.GetComponent<Rigidbody>().velocity.magnitude;
if (s<speed)
patrol.GetComponent<Rigidbody>().AddForce( dir/dir.magnitude *ac );
patrol.transform.LookAt(new Vector3(target.x,patrol.transform.position.y,target.z));
}
}
物体管理类,里面是挂载到各个物体上的代码。
MeController是管理主角用的。里面提供了按键移动的函数(WSAD移动)。在LateUpdate中会调节主角的面向,使主角始终面向她移动的方向。
PatrolController类管理所有的巡逻兵。Move函数会让巡逻兵向一个目标移动。
CreateRoute生成路径,生成一个3-5边型的凸多边形,巡逻兵会沿着边巡逻。如果巡逻兵跟丢主角,会在原地周围重新生成一个多边形并且移动。
OnCollisionStay函数处理巡逻兵的碰撞,碰撞到障碍物会前往下一个节点。碰撞到主角后会结束游戏。
项目亮点
可爱,凄美,引人遐想
这是我们的主角:
效果如下图:
通过微妙的点光源,让少女在漆黑的迷宫中逃生,黑暗中潜伏着怪物。
让人感觉很有意境,仿佛深夜里XXXXXXXXXXX
全部采用物理引擎,移动更丝滑
主角和巡逻兵,还有障碍物,墙壁都用了物理引擎。
所有角色的移动都是用FixedUpdate里面通过AddForce来实现。
移动时可以清晰地看到加速和减速过程。更符合实际。
对Rigidbody.velocity的新理解
许多新人在用刚体时喜欢直接更改刚体的velocity来改变刚体的速度。这样看上去很方便。
但是在做项目的同时经过实验,发现velocity根本就不是真实的速度。
在编程的时候,如果经常使用更改velocity的方法,那么在发生过大量碰撞和摩擦后,velocity的值会偏离真是的速度。也就是说,velocity会被不符合物理逻辑的强制更改打乱。
所以在使用物理引擎的时候,更改速度还是用AddForce等符合物理逻辑的方法,尽量不要使用velocity的方法。
代码链接
项目下载
使用方法:下载,解压。创建新项目,用我的Assets覆盖你的Assets,在unity中打开SampleScene,运行即可。