簡答題
- 遊戲對象運動的本質是什麼?
- 請用三種方法以上方法,實現物體的拋物線運動。(如,修改Transform屬性,使用向量Vector3的方法…)
- 寫一個程序,實現一個完整的太陽系, 其他星球圍繞太陽的轉速必須不一樣,且不在一個法平面上。
遊戲對象運動的本質
遊戲對象的運動實際上是通過不斷更改位置(position)屬性和旋轉(rotation)屬性的值,以微小差異的離散值變化,在快速刷新圖像的條件下,使人視覺上產生運動的感覺。
// 以一定速度向左移動
this.transform.position += speed * Vector3.left * Time.deltaTime;
// 以一定速度向e方向旋轉
Quaternion q = Quaternion.AngleAxis(speed * Time.deltaTime, e);
this.transform.localRotation *= q;
物體拋物線運動
拋物線運動可以分解爲水平方向運動以及垂直方向運動,水平方向速度一定,垂直方向存在加速度。但是由於顯示畫面刷新快且時間間隔一定,所以可以認爲在每一次 Update
函數執行時速度是不變的,存在公式ds=dv*dt
,關鍵是垂直方向在此時也可以認爲速度不變。
(1) 第一種方法:在原position上增值
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class NewBehaviourScript : MonoBehaviour
{
public float v = 1;
public float a = 1;
// Start is called before the first frame update
void Start()
{
Debug.Log("Start!");
}
// Update is called once per frame
void Update()
{
Debug.Log("Update!");
this.transform.position += Vector3.down * Time.deltaTime * v; // 豎直方向和水平方向都有 ds=dv*dt
this.transform.position += Vector3.right * Time.deltaTime * 2;
v += a; // 存在加速度,v的值不斷改變
}
}
(2) 第二種方法:用新的position取代原position
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class NewBehaviourScript : MonoBehaviour
{
public float v = 1; //垂直方向速度
public float a = 1;
// Start is called before the first frame update
void Start()
{
Debug.Log("Start!");
}
// Update is called once per frame
void Update()
{
Debug.Log("Update!");
this.transform.position = new Vector3(this.transform.position.x + v, this.transform.position.y, this.transform.position.z + 1);
v += a; // 存在加速度,v的值不斷改變
}
}
(3) 第三種方法:使用translate函數
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class NewBehaviourScript : MonoBehaviour
{
public float v = 1; // 垂直方向速度
public float a = 1;
// Start is called before the first frame update
void Start()
{
Debug.Log("Start!");
}
// Update is called once per frame
void Update()
{
Debug.Log("Update!");
Vector3 run = new Vector3(Time.deltaTime * 2, -Time.deltaTime * v, 0); // 和速度向量
this.transform.Translate(run);
v += a; // 存在加速度,v的值不斷改變
}
}
完整太陽系
在unity中建立模型(紅色爲太陽,藍色爲地球):
Moon需要放在Earth下,作爲子對象,以保證在Earth自轉時,軸位置的改變不會影響到Moon的公轉。
在空遊戲對象中綁定腳本:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class NewBehaviourScript1 : MonoBehaviour
{
GameObject Sun, Mer, Venus, Mars, Jupiter, Saturn, Uranus, Neptune, Earth, Moon;
// Start is called before the first frame update
void Start()
{
Sun = GameObject.Find("Sun");
Mer= GameObject.Find("Mer");
Venus= GameObject.Find("Venus");
Mars= GameObject.Find("Mars");
Jupiter= GameObject.Find("Jupiter");
Saturn= GameObject.Find("Saturn");
Uranus= GameObject.Find("Uranus");
Neptune= GameObject.Find("Neptune");
Earth= GameObject.Find("Earth");
Moon= GameObject.Find("Moon");
}
// Update is called once per frame
void Update()
{
Earth.transform.RotateAround(Sun.transform.position, Vector3.up, 50 * Time.deltaTime);
Earth.transform.Rotate(Vector3.up * 30 * Time.deltaTime);
Moon.transform.RotateAround(Earth.transform.position, Vector3.up, 359 * Time.deltaTime);
Moon.transform.Rotate(Vector3.up * 30 * Time.deltaTime);
Mer.transform.RotateAround(Sun.transform.position, new Vector3(1, 5, 2), 45 * Time.deltaTime);
Mer.transform.Rotate(Vector3.up * 30 * Time.deltaTime);
Venus.transform.RotateAround(Sun.transform.position, new Vector3(1, 2, 0), 42 * Time.deltaTime);
Venus.transform.Rotate(Vector3.up * 30 * Time.deltaTime);
Mars.transform.RotateAround(Sun.transform.position, new Vector3(2, 1, 2), 40 * Time.deltaTime);
Mars.transform.Rotate(Vector3.up * 30 * Time.deltaTime);
Jupiter.transform.RotateAround(Sun.transform.position, new Vector3(1, 2, 1), 38 * Time.deltaTime);
Jupiter.transform.Rotate(Vector3.up * 30 * Time.deltaTime);
Saturn.transform.RotateAround(Sun.transform.position, new Vector3(2, 1, 1), 36 * Time.deltaTime);
Saturn.transform.Rotate(Vector3.up * 30 * Time.deltaTime);
Uranus.transform.RotateAround(Sun.transform.position, new Vector3(3, 1, 2), 35 * Time.deltaTime);
Uranus.transform.Rotate(Vector3.up * 30 * Time.deltaTime);
Neptune.transform.RotateAround(Sun.transform.position, new Vector3(1, 3, 2), 33 * Time.deltaTime);
Neptune.transform.Rotate(Vector3.up * 30 * Time.deltaTime);
}
}
遊戲運行結果如下:
編程實踐
- 列出遊戲中提及的事物(Objects)
- 用表格列出玩家動作表(規則表),注意,動作越少越好
- 請將遊戲中對象做成預製
- 在 GenGameObjects 中創建 長方形、正方形、球 及其色彩代表遊戲中的對象。
- 使用 C# 集合類型 有效組織對象
- 整個遊戲僅 主攝像機 和 一個 Empty 對象, 其他對象必須代碼動態生成!!! 。 整個遊戲不許出現 Find 遊戲對象, SendMessage 這類突破程序結構的 通訊耦合 語句。 違背本條準則,不給分
請使用課件架構圖編程,不接受非 MVC 結構程序 - 注意細節,例如:船未靠岸,牧師與魔鬼上下船運動中,均不能接受用戶事件!
遊戲中提及的事物
- 河岸,牧師、惡魔、船、河水、“Go”按鈕、“Restart”按鈕。
玩家動作表
動作 | 參數 | 結果 |
---|---|---|
點擊牧師或惡魔 | 船不在移動且兩者在同一側 | 牧師或惡魔上船或下船 |
點擊小船 | 小船已靠岸 ,船上有人物 | 小船向另一邊航行 |
遊戲對象預製
下載了一些材料,使用Cube和Sphere預製了河岸、牧師、惡魔、小船。
遊戲實現
主要按照MVC結構設置了Model、Controller以及View。其中Controller被分解成了Director、SceneController、UserActions幾個方面。
程序內主要需要完成的是根據行爲爲事物添加動作,關鍵在於position的定位以及狀態的設定。
遇到的一些問題主要有:
1.空位判斷時,需要保證小船的空位數組在起始處與終點處時相互對應的。
2.綁定事件到遊戲對象需要,先爲事件設計類,再將事件類實例使用AddComponent(typeof(事件類)) as 事件類
綁定到遊戲對象。
3.Resume功能必須詳盡的考慮到所有可能被改變的初始狀況。
4.靈活使用單例,可以方便的將各個組件串聯到一起,本次實驗中SSDirector.getInstance().currentSceneController as 類
就是一個很明顯的例子,通過SSDirector單例,可以在其他類中對場景進行唯一性的變換。
5.View部分應該通過用戶行爲與場記交互,從而影響場景變換。
程序運行界面:
程序運行效果視頻:https://www.bilibili.com/video/av68226387/
具體程序如下:
1.View.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UserActions;
using Director;
namespace View
{
public class pageImage : MonoBehaviour
{
IUserAction userAction;
public bool isOver = true;
// Start is called before the first frame update
void Start()
{
userAction = SSDirector.getInstance().currentSceneController as IUserAction;
}
private void OnGUI()
{
if (!isOver)
{
GUI.Label(new Rect(Screen.width / 2 - 80, Screen.height / 2 - 80, 100, 50), "Gameover!", new GUIStyle(){fontSize = 30});
if (GUI.Button(new Rect(Screen.width / 2 - 60, Screen.height / 2, 100, 50), "Restart"))
{
userAction.Resume();
isOver = true;
}
}
if(GUI.Button(new Rect(Screen.width / 2- 60, Screen.height / 2-150, 100, 50), "Go"))
{
userAction.MoveBoat();
}
}
}
}
2.UserActions.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Director;
using Models;
namespace UserActions
{
public interface IUserAction
{
void MoveBoat(); /* 小船向另一邊航行,條件:小船已靠岸 ,船上有人物 */
void MoveRole(Role role); /* 牧師或惡魔上船或下船,條件:船不在移動且兩者在同一側 */
bool Check(); /* 遊戲結束,條件:岸上牧師的人數小於惡魔數; 通關,條件:所有人物都達到河岸另一邊 */
void Resume(); /* 遊戲重新開始 */
}
}
3.SceneController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/* 場記命名空間,便於被Director.cs文件中引用,場記應該受導演管制和調用 */
namespace SceneController {
/* 場記接口:用於被場記類繼承,一個遊戲中通常不止一個場記 */
public interface ISceneController
{
void LoadResource(); /* 加載當前場景的資源 */
//void Pause(); /* 當前場景暫停 */
//void Resume(); /* 當前場景繼續進行 */
}
}
4.Model.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Director;
using SceneController;
using UserActions;
namespace Models
{
public class Land /* 陸地類 */
{
GameObject land; /* 聲明陸地對象 */
Vector3[] rolesPositions; /* 保存陸地上人物的位置 */
bool isStart; /* 標記左右岸,默認右岸爲Start */
Vector3 landPosition; /* 岸的位置 */
bool[] isPosEmpty = new bool[6]; /* 保存岸上位置的佔用的情況 */
public Land(bool startOrEnd) /* true爲右岸,false爲左岸 */
{
if (startOrEnd)
{
landPosition = new Vector3(3, -1, -2);
rolesPositions = new Vector3[] {new Vector3(1,1,-4), new Vector3(1,1,-2), new Vector3(2,1,-4),
new Vector3(2,1,-2),new Vector3(3,1,-4),new Vector3(3,1,-2)};
}
else
{
landPosition = new Vector3(-20, -1, -2);
rolesPositions = new Vector3[] {new Vector3(-18,1,-4), new Vector3(-18,1,-2), new Vector3(-19,1,-4),
new Vector3(-19,1,-2),new Vector3(-20,1,-4),new Vector3(-20,1,-2)};
}
land = Object.Instantiate(Resources.Load("Land", typeof(GameObject)), landPosition, Quaternion.identity) as GameObject;
isStart = startOrEnd;
for (int i = 0; i < 6; i++) isPosEmpty[i] = true;
}
public Vector3 getPos(int index) /* 獲得一個岸上的位置 */
{
return rolesPositions[index];
}
public int getAnEmptyIndex()
{
for(int i = 0; i < isPosEmpty.Length; i++)
{
if (isPosEmpty[i])
return i;
}
return -1;
}
public bool isEmpty() /* 岸上是否爲空 */
{
for(int i = 0; i < isPosEmpty.Length; i++)
{
if (!isPosEmpty[i])
return false;
}
return true;
}
public bool judgeId() /* 判斷是左岸還是右岸 */
{
return isStart;
}
public void setOccupied(int index) /* 將位置設爲佔用 */
{
isPosEmpty[index] = false;
}
public void setEmpty(int index) /* 將位置設置爲空閒 */
{
isPosEmpty[index] = true;
}
public int getSeatByPos(Vector3 pos) /* 根據座標獲得人物在岸上的位置 */
{
for(int i = 0; i < rolesPositions.Length; i++)
{
if (rolesPositions[i] == pos)
return i;
}
return -1;
}
public void reset() /* 所有位置置空 */
{
for (int i = 0; i < isPosEmpty.Length; i++)
{
isPosEmpty[i] = true;
}
}
}
public class Boat /* 小船類 */
{
GameObject boat;
Move move;
Click click;
Vector3[] rolePositionsAtStart; /* 小船在右岸時的座位位置 */
Vector3[] rolePositionsAtEnd; /* 小船在左岸時的座位位置 */
bool[] isPosEmpty = new bool[2]; /* 座位是否空閒 */
bool boatPosition; /* 小船的位置,右岸爲true,左岸爲false */
Vector3 startPos, endPos; /* 小船在起點和終點時的位置 */
public Boat()
{
/* 起點和終點的船上位置座標必須對應,調試了好久!! */
startPos = new Vector3(-1, -1, -1);
rolePositionsAtStart = new Vector3[] { new Vector3(-0.5F, 0.5F, -1), new Vector3(-1.6F, 0.5F, -1) };
endPos = new Vector3(-16, -1, -1);
rolePositionsAtEnd = new Vector3[] { new Vector3(-15.5F, 0.5F, -1), new Vector3(-16.6F, 0.5F, -1) };
boatPosition = true;
boat= Object.Instantiate(Resources.Load("Boat", typeof(GameObject)), startPos, Quaternion.identity) as GameObject;
move = boat.AddComponent(typeof(Move)) as Move;
click = boat.AddComponent(typeof(Click)) as Click;
move.moveTowards(startPos);
for (int i = 0; i < isPosEmpty.Length; i++) isPosEmpty[i] = true;
}
public Vector3 getPos(int index) /* 獲得一個岸上的位置 */
{
if (boatPosition)
return rolePositionsAtStart[index];
else
return rolePositionsAtEnd[index];
}
public int getAnEmptyIndex()
{
for (int i = 0; i < isPosEmpty.Length; i++)
{
if (isPosEmpty[i])
return i;
}
return -1;
}
public int getSeatByPos(Vector3 pos) /* 根據座標獲得人物在船上的位置 */
{
if (boatPosition)
{
for (int i = 0; i < 2; i++)
{
if (rolePositionsAtStart[i] == pos)
return i;
}
}
else
{
for (int i = 0; i < 2; i++)
{
if (rolePositionsAtEnd[i] == pos)
return i;
}
}
return -1;
}
public void setOccupied(int index) /* 將位置設爲佔用 */
{
isPosEmpty[index] = false;
}
public void setEmpty(int index) /* 將位置設置爲空閒 */
{
isPosEmpty[index] = true;
}
public bool isEmpty() /* 船上是否爲空 */
{
for (int i = 0; i < isPosEmpty.Length; i++)
{
if (!isPosEmpty[i])
{
return false;
}
}
return true;
}
public bool isMove()
{
if (boat.transform.position != startPos && boat.transform.position != endPos)
return true;
else
return false;
}
public void moveToOtherSide() /* 船向對岸移動 */
{
if (getBoatPos())
{
move.moveTowards(endPos);
boatPosition = false;
}
else
{
move.moveTowards(startPos);
boatPosition = true;
}
}
public bool getBoatPos() /* 返回船在左岸還是右岸,右岸爲true */
{
return boatPosition;
}
public GameObject getPrototype() /* 獲得遊戲對象 */
{
return boat;
}
public void reset() /* 所有位置置空 */
{
for(int i = 0; i < isPosEmpty.Length; i++)
{
isPosEmpty[i] = true;
}
boat.transform.position = startPos;
boatPosition = true;
}
}
public class Role /* 人物類 */
{
GameObject role;
Move move; /* 移動 */
Click click;
bool id; /* 判斷身份,牧師或者惡魔,true爲牧師,false爲惡魔 */
int isOnLand; /* 判斷是否在岸上,在船上是0,右岸爲1,左岸爲2 */
Land startLand = (SSDirector.getInstance().currentSceneController as Controller).startLand;
Land endLand = (SSDirector.getInstance().currentSceneController as Controller).endLand;
Boat boat = (SSDirector.getInstance().currentSceneController as Controller).boat;
public Role(bool hisId,string name) /* 用name來標識人物 */
{
int emptyIndex = startLand.getAnEmptyIndex();
if (emptyIndex == -1)
Application.Quit();
if (hisId)
{
role = Object.Instantiate(Resources.Load("Priest", typeof(GameObject)), startLand.getPos(emptyIndex), Quaternion.identity) as GameObject;
startLand.setOccupied(emptyIndex);
}
else
{
role = Object.Instantiate(Resources.Load("Devil", typeof(GameObject)), startLand.getPos(emptyIndex), Quaternion.identity) as GameObject;
startLand.setOccupied(emptyIndex);
}
role.name = name;
isOnLand = 1;
id = hisId;
move = role.AddComponent(typeof(Move)) as Move;
click = role.AddComponent(typeof(Click)) as Click;
move.moveTowards(startLand.getPos(emptyIndex));
click.SetRole(this);
}
public void setRolePos(Vector3 pos) /* 設置人物位置 */
{
role.transform.position = pos;
}
public void goAshore() /* 人物上岸 */
{
Land land = boat.getBoatPos() ? startLand : endLand;
int pos = land.getAnEmptyIndex();
if (pos == -1)
{
Debug.Log("Land No Empty.");
return;
}
land.setOccupied(pos);
boat.setEmpty(boat.getSeatByPos(role.transform.position));
//Debug.Log("Boat empty:" + boat.getSeatByPos(role.transform.position) + role.transform.position);
move.moveTowards(land.getPos(pos));
if (boat.getBoatPos()) isOnLand = 1;
else isOnLand = 2;
role.transform.parent = null;
}
public void goBoarding() /* 人物上船 */
{
Land land = boat.getBoatPos() ? startLand : endLand;
int pos = boat.getAnEmptyIndex();
if (pos == -1)
{
Debug.Log("Boat No Empty.");
return;
}
boat.setOccupied(pos);
land.setEmpty(land.getSeatByPos(role.transform.position));
move.moveTowards(boat.getPos(pos));
isOnLand = 0;
role.transform.parent = boat.getPrototype().transform;
}
public int getRolePos() /* 瞭解人物在左岸(2)、右岸(1)還是船上(0) */
{
return isOnLand;
}
public bool getRoleId() /* 獲得人物身份 */
{
return id;
}
public void reset()
{
int emptyIndex = startLand.getAnEmptyIndex();
setRolePos(startLand.getPos(emptyIndex));
startLand.setOccupied(emptyIndex);
role.transform.parent = null;
isOnLand = 1;
}
}
public class Move: MonoBehaviour
{
int speed = 50;
Vector3 destPos;
bool canMove = false;
private void Update()
{
if (canMove)
{
transform.position = Vector3.MoveTowards(transform.position, destPos, speed * Time.deltaTime);
if(transform.position==destPos)
canMove = false;
}
}
public void moveTowards(Vector3 dest)
{
destPos = dest;
canMove = true;
}
}
public class Click : MonoBehaviour
{
IUserAction userAction;
Role role;
public void SetRole(Role role)
{
this.role = role;
}
public void Start()
{
userAction = SSDirector.getInstance().currentSceneController as IUserAction;
}
private void OnMouseDown()
{
if (role == null) return;
else
{
userAction.MoveRole(role);
}
}
}
}
5.Director.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using SceneController;
namespace Director
{
public class SSDirector : System.Object
{
/* 單例模式,導演只有一個 */
private static SSDirector _instance;
/* 記錄當前的場記,也即記錄了當前場景 */
public ISceneController currentSceneController { get; set; }
/* 記錄遊戲是否正在運行 */
public bool running { get; set; }
/* 獲得單例 */
public static SSDirector getInstance()
{
if (_instance == null)
{
_instance = new SSDirector();
}
return _instance;
}
}
}
6.Controller.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Director;
using UserActions;
using Models;
using View;
using SceneController;
/* 場記類實現:需要繼承場記接口 */
public class Controller : MonoBehaviour, ISceneController, IUserAction
{
public Land startLand, endLand;
public Boat boat;
public Role[] roles;
public pageImage gameGUI;
/* 場記接口方法的實現,主要完成場景載入 */
public void LoadResource()
{
startLand = new Land(true);
endLand = new Land(false);
boat = new Boat();
roles = new Role[6];
for (int i = 0; i < 3; i++)
{
roles[i] = new Role(true, "Priest" + i);
}
for (int i = 3; i < 6; i++)
{
roles[i] = new Role(false, "Devil" + i);
}
}
/* 用戶行爲接口方法的實現 */
public void MoveBoat()
{
if (boat.isEmpty() || boat.isMove()|| !gameGUI.isOver) return;
boat.moveToOtherSide();
gameGUI.isOver = Check();
}
public void MoveRole(Role role)
{
if (!boat.isMove() && gameGUI.isOver)
{
if (role.getRolePos() == 0)
{
role.goAshore();
}
else
{
role.goBoarding();
}
}
}
public bool Check()
{
int priestNumAtStart = 0;
int priestNumAtEnd = 0;
int devilNumAtStart = 0;
int devilNumAtEnd = 0;
int priestNumAtBoat = 0;
int devilNumAtBoat = 0;
for (int i = 0; i < 6; i++)
{
switch (roles[i].getRolePos())
{
case 0:
if (roles[i].getRoleId())
{
priestNumAtBoat++;
}
else
{
devilNumAtBoat++;
}
break;
case 1:
if (roles[i].getRoleId())
{
priestNumAtStart++;
}
else
{
devilNumAtStart++;
}
break;
case 2:
if (roles[i].getRoleId())
{
priestNumAtEnd++;
}
else
{
devilNumAtEnd++;
}
break;
default:
break;
}
}
Debug.Log("priestNumAtBoat:" + priestNumAtBoat);
Debug.Log("priestNumAtStart:" + priestNumAtStart);
Debug.Log("devilNumAtBoat:" + devilNumAtBoat);
Debug.Log("devilNumAtStart:" + devilNumAtStart);
Debug.Log("priestNumAtEnd:" + priestNumAtEnd);
Debug.Log("devilNumAtEnd:" + devilNumAtEnd);
Debug.Log("");
if ((priestNumAtBoat + priestNumAtStart < devilNumAtBoat + devilNumAtStart || priestNumAtBoat + priestNumAtEnd < devilNumAtBoat + devilNumAtEnd))
{
return false;
}
else
return true;
}
public void Resume()
{
startLand.reset();
endLand.reset();
boat.reset();
for (int i = 0; i < roles.Length; i++)
{
roles[i].reset();
}
}
/* 控制器的生命週期 */
void Awake()
{
SSDirector director = SSDirector.getInstance();
director.currentSceneController = this;
director.currentSceneController.LoadResource();
}
void Start()
{
Debug.Log("First SceneController Start!");
gameGUI = gameObject.AddComponent<pageImage>() as pageImage;
}
}