3D Game Programming & Design:空間與運動

空間與運動

1、簡答題

簡答並用程序驗證【建議做】

遊戲對象運動的本質是什麼?

遊戲對象運動的本質,是它隨着頁面刷新而產生的連續的運動,包括遊戲對象的位置、旋轉、角度、大小等。

請用三種方法以上方法,實現物體的拋物線運動。

(如,修改Transform屬性,使用向量Vector3的方法…)

  • 我們知道拋物線運動也就是平拋運動的速度運算公式如下:

水平初速度Vx = V0
豎直方向速度Vy = gt
其中g是重力加速度

在Unity3D的三維座標系中,我們以xoz座標系爲例,分別用修改Transform屬性、使用向量Vector3的方法、 的方法來實現物體的拋物線運動。

方法一:修改Transform屬性

物體在水平方向上速度不變,豎直方向上的速度由於重力而加速。我們根據平拋運動速度變化的特點在每個時間區間內改變transform的position屬性即可,代碼如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class NewBehaviourScript : MonoBehaviour
{
    public float xspeed = 5;//水平方向初速度
    public float zspeed = 0;
    public float g = 10;//重力加速度
    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        this.transform.position += Vector3.right * Time.deltaTime * xspeed;
        this.transform.position += Vector3.down * Time.deltaTime * zspeed;
        zspeed += g * Time.deltaTime;
    }
}

Transform.right moves the GameObject in the red arrow’s axis (X).

方法二:使用向量Vector3

創建Vector3向量myVector,把每次position的改變疊加到該向量上,從而改變豎直方向上的速度,代碼如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class vector : MonoBehaviour
{
    public float xspeed = 5;//水平方向初速度
    public float zspeed = 0;
    public float g = 10;//重力加速度
    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        Vector3 myVector = new Vector3(Time.deltaTime * xspeed, -Time.deltaTime * zspeed, 0);
        this.transform.position += myVector;
        zspeed += g * Time.deltaTime;
    }
}

public Vector3(float x, float y, float z);
//Creates a new vector with given x, y components and sets z to zero.

方法三:使用transform.Translate方法

方法三和二很像,也需要先創建一個向量,唯一的區別是這裏通過translate方法來計算每一時刻的速度的,代碼如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class trans : MonoBehaviour
{
    public float xspeed = 5;//水平方向初速度
    public float zspeed = 0;
    public float g = 10;//重力加速度
    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        Vector3 myVector = new Vector3(Time.deltaTime * xspeed, -Time.deltaTime * zspeed, 0);
        this.transform.Translate(myVector);
        zspeed += g * Time.deltaTime;
    }
}

寫一個程序,實現一個完整的太陽系。

其他星球圍繞太陽的轉速必須不一樣,且不在一個法平面上。
代碼如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class solar : MonoBehaviour
{
	// Start is called before the first frame update
	public Transform sun;
	public Transform mars;
	public Transform jupiter;
	public Transform saturn;
	public Transform uranus;
	public Transform neptune;
	public Transform mercury;
	public Transform venus;
	public Transform earth;

	void Start()
	{
		//init
		sun.position = Vector3.zero;
		mercury.position = new Vector3(1, 0, 0);
		venus.position = new Vector3(-2, 0, 0);
		earth.position = new Vector3(3, 0, 0);
		mars.position = new Vector3(-6, 0, 0);
		jupiter.position = new Vector3(-7, 0, 0);
		saturn.position = new Vector3(9, 0, 0);
		uranus.position = new Vector3(12, 0, 0);
		neptune.position = new Vector3(-14, 0, 0);
	}
	// Update is called once per frame
	void Update()
	{
		mars.RotateAround(sun.position, new Vector3(0, 13, 5), 10 * Time.deltaTime);
		mars.Rotate(new Vector3(0, 12, 5) * 40 * Time.deltaTime);

		jupiter.RotateAround(sun.position, new Vector3(0, 8, 3), 8 * Time.deltaTime);
		jupiter.Rotate(new Vector3(0, 10, 3) * 30 * Time.deltaTime);

		saturn.RotateAround(sun.position, new Vector3(0, 2, 1), 6 * Time.deltaTime);
		saturn.Rotate(new Vector3(0, 3, 1) * 20 * Time.deltaTime);

		uranus.RotateAround(sun.position, new Vector3(0, 9, 1), 6 * Time.deltaTime);
		uranus.Rotate(new Vector3(0, 10, 1) * 20 * Time.deltaTime);

		neptune.RotateAround(sun.position, new Vector3(0, 7, 1), 5 * Time.deltaTime);
		neptune.Rotate(new Vector3(0, 8, 1) * 30 * Time.deltaTime);

		mercury.RotateAround(sun.position, new Vector3(0, 3, 1), 20 * Time.deltaTime);
		mercury.Rotate(new Vector3(0, 5, 1) * 5 * Time.deltaTime);

		venus.RotateAround(sun.position, new Vector3(0, 2, 1), 15 * Time.deltaTime);
		venus.Rotate(new Vector3(0, 2, 1) * Time.deltaTime);

		earth.RotateAround(sun.position, Vector3.up, 10 * Time.deltaTime);
		earth.Rotate(Vector3.up * 30 * Time.deltaTime);

	}
}

效果圖如下:
在這裏插入圖片描述

2、編程實踐

閱讀以下游戲腳本

Priests and Devils

Priests and Devils is a puzzle game in which you will help the Priests and Devils to cross the river within the time limit. There are 3 priests and 3 devils at one side of the river. They all want to get to the other side of this river, but there is only one boat and this boat can only carry two persons each time. And there must be one person steering the boat from one side to the other side. In the flash game, you can click on them to move them and click the go button to move the boat to the other direction. If the priests are out numbered by the devils on either side of the river, they get killed and the game is over. You can try it in many > ways. Keep all priests alive! Good luck!

程序需要滿足的要求及實現情況

  • play the game ( http://www.flash-game.net/game/2535/priests-and-devils.html )
  • 列出遊戲中提及的事物(Objects)

Characters: Priests牧師、Devils魔鬼
Environment: Boat船、River河流、Coast岸

  • 用表格列出玩家動作表(規則表),注意,動作越少越好

動作 條件
開船 船上有人,船在左岸或者右岸
下船 船上有人,船到左岸或者右岸
開始岸牧師上船 船在開始岸,船有空位,開始岸有牧師
結束岸牧師上船 船在結束岸,船有空位,結束岸有牧師
開始岸魔鬼上船 船在開始岸,船有空位,開始岸有魔鬼
結束岸魔鬼上船 船在結束岸,船有空位,結束岸有魔鬼
  • 在 GenGameObjects 中創建 長方形、正方形、球 及其色彩代表遊戲中的對象,將遊戲中對象做成預製:
    在這裏插入圖片描述

  • 整個遊戲僅主攝像機和一個 Empty 對象, 其他對象必須代碼動態生成。

  • 請使用課件架構圖編程,不接受非 MVC 結構程序
    在這裏首先對MVC結構做一個解釋說明:
    MVC 結構

    MVC全名是Model View Controller,是模型(model)-視圖(view)-控制器(controller)的縮寫,一種軟件設計典範,用一種業務邏輯、數據、界面顯示分離的方法組織代碼,將業務邏輯聚集到一個部件裏面,在改進和個性化定製界面及用戶交互的同時,不需要重新編寫業務邏輯。MVC被獨特的發展起來用於映射傳統的輸入、處理和輸出功能在一個邏輯的圖形化用戶界面的結構中。

    在本遊戲中的運用
    本遊戲中所涉及的objects,包括牧師與魔鬼以及其他的所有環境的game objects都是model,它們分別通過不同的controller類控制。除了控制各個object的controller以外,還有控制整個場景的FirstController控制着這個場景中的所有對象。和一個控制着場景的創建、切換、銷燬、遊戲暫停、遊戲退出的Controller類。

基於MVC 結構的代碼分析

Model & Controller部分
這部分主要分爲兩個小的分支,用於控制移動和用於具體控制各個object。
Moveable是一個可以掛載在GameObject上的類,Controller通過setDestination()讓GameObject移動起來。

  • moveable:用於控制角色和船的移動。
public class Moveable: MonoBehaviour {
		
		readonly float move_speed = 20;
		// change frequently
		int moving_status;	// 0->not moving, 1->moving to middle, 2->moving to dest
		Vector3 dest;
		Vector3 middle;
        //更新移動狀態
		void Update() {
			if (moving_status == 1) {
				transform.position = Vector3.MoveTowards (transform.position, middle, move_speed * Time.deltaTime);
				if (transform.position == middle) {
					moving_status = 2;
				}
			} else if (moving_status == 2) {
				transform.position = Vector3.MoveTowards (transform.position, dest, move_speed * Time.deltaTime);
				if (transform.position == dest) {
					moving_status = 0;
				}
			}
		}
        //確定終點coast
		public void setDestination(Vector3 _dest) {
			dest = _dest;
			middle = _dest;
			if (_dest.y == transform.position.y) {	// boat moving
				moving_status = 2;
			}
			else if (_dest.y < transform.position.y) {	// character from coast to boat
				middle.y = transform.position.y;
			} else {								// character from boat to coast
				middle.x = transform.position.x;
			}
			moving_status = 1;
		}

		public void reset() {
			moving_status = 0;
		}
	}
  • CoastController:用於控制與河岸有關的動作,比如角色上下岸,船的離開和停靠。

CoastController,BoatController和MyCharacterController都是類似的,這裏我們就以CoastController爲例。首先封裝了船和河岸的GameObject,在構造函數中實例化了一個perfab,並對河岸的位置進行一些初始化的設置。此外還提供了一些方法的定義使得場景控制器能夠調用它們。

 public class CoastController {
		readonly GameObject coast;
		readonly Vector3 from_pos = new Vector3(9,1,0);
		readonly Vector3 to_pos = new Vector3(-9,1,0);
		readonly Vector3[] positions;
		readonly int to_or_from;	// to->-1, from->1

		// change frequently
		MyCharacterController[] passengerPlaner;

		public CoastController(string _to_or_from) {
			positions = new Vector3[] {new Vector3(6.5F,2.25F,0), new Vector3(7.5F,2.25F,0), new Vector3(8.5F,2.25F,0), 
				new Vector3(9.5F,2.25F,0), new Vector3(10.5F,2.25F,0), new Vector3(11.5F,2.25F,0)};

			passengerPlaner = new MyCharacterController[6];

			if (_to_or_from == "from") {
				coast = Object.Instantiate (Resources.Load ("Perfabs/Stone", typeof(GameObject)), from_pos, Quaternion.identity, null) as GameObject;
				coast.name = "from";
				to_or_from = 1;
			} else {
				coast = Object.Instantiate (Resources.Load ("Perfabs/Stone", typeof(GameObject)), to_pos, Quaternion.identity, null) as GameObject;
				coast.name = "to";
				to_or_from = -1;
			}
		}

		public int getEmptyIndex() {
			for (int i = 0; i < passengerPlaner.Length; i++) {
				if (passengerPlaner [i] == null) {
					return i;
				}
			}
			return -1;
		}

		public Vector3 getEmptyPosition() {
			Vector3 pos = positions [getEmptyIndex ()];
			pos.x *= to_or_from;
			return pos;
		}

		public void getOnCoast(MyCharacterController characterCtrl) {
			int index = getEmptyIndex ();
			passengerPlaner [index] = characterCtrl;
		}

		public MyCharacterController getOffCoast(string passenger_name) {	// 0->priest, 1->devil
			for (int i = 0; i < passengerPlaner.Length; i++) {
				if (passengerPlaner [i] != null && passengerPlaner [i].getName () == passenger_name) {
					MyCharacterController charactorCtrl = passengerPlaner [i];
					passengerPlaner [i] = null;
					return charactorCtrl;
				}
			}
			Debug.Log ("cant find passenger on coast: " + passenger_name);
			return null;
		}

		public int get_to_or_from() {
			return to_or_from;
		}

		public int[] getCharacterNum() {
			int[] count = {0, 0};
			for (int i = 0; i < passengerPlaner.Length; i++) {
				if (passengerPlaner [i] == null)
					continue;
				if (passengerPlaner [i].getType () == 0) {	// 0->priest, 1->devil
					count[0]++;
				} else {
					count[1]++;
				}
			}
			return count;
		}

		public void reset() {
			passengerPlaner = new MyCharacterController[6];
		}
	}

這些方法中比較難理解的是getEmptyPosition()方法,這是因爲character牧師和魔鬼要站在boat上,所以這裏需要提前留出空位,讓遊戲角色能夠移動到合適的位置。

  • MyCharacterController:用於控制6個角色的動作,比如上船,上岸等。
  • BoatController:用於控制船的運動以及角色的上下船綁定。

因爲實現原理和CoastController類似,就不再贅述了,代碼見GitHub。

控制器之間的邏輯關係

  • Director控制器:它就像是整個遊戲的“導演”,場景的加載、切換等,也可以控制遊戲暫停、結束等等。它只有一個實例,之後的任何返回值和各個方法之間的通信都會返回到這個實例對象上。
public class Director : System.Object {
		private static Director _instance;
		public SceneController currentSceneController { get; set; }

		public static Director getInstance() {
			if (_instance == null) {
				_instance = new Director ();
			}
			return _instance;
		}
	}
  • SceneController接口:上面Director類中的public SceneController currentSceneController { get; set; }就是SceneController接口的實現。Director通過調用SceneController接口中的方法,來實現對場景的控制權。
public interface SceneController {
		void loadResources ();
	}

而接口不能直接創建對象,因此它必須要有一個繼承它的類,這個類就是FirstController。

  • FirstController:FirstController實現了具體的對遊戲整體場景的控制方法。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Com.Mygame;

public class FirstController : MonoBehaviour, SceneController, UserAction {

	readonly Vector3 water_pos = new Vector3(0,0.5F,0);


	UserGUI userGUI;

	public CoastController fromCoast;
	public CoastController toCoast;
	public BoatController boat;
	private MyCharacterController[] characters;

	void Awake() {
		Director director = Director.getInstance ();
		director.currentSceneController = this;
		userGUI = gameObject.AddComponent <UserGUI>() as UserGUI;
		characters = new MyCharacterController[6];
		loadResources ();
	}

	public void loadResources() {
		GameObject water = Instantiate (Resources.Load ("Perfabs/Water", typeof(GameObject)), water_pos, Quaternion.identity, null) as GameObject;
		water.name = "water";

		fromCoast = new CoastController ("from");
		toCoast = new CoastController ("to");
		boat = new BoatController ();

		loadCharacter ();
	}

	private void loadCharacter() {
		for (int i = 0; i < 3; i++) {
			MyCharacterController cha = new MyCharacterController ("priest");
			cha.setName("priest" + i);
			cha.setPosition (fromCoast.getEmptyPosition ());
			cha.getOnCoast (fromCoast);
			fromCoast.getOnCoast (cha);

			characters [i] = cha;
		}

		for (int i = 0; i < 3; i++) {
			MyCharacterController cha = new MyCharacterController ("devil");
			cha.setName("devil" + i);
			cha.setPosition (fromCoast.getEmptyPosition ());
			cha.getOnCoast (fromCoast);
			fromCoast.getOnCoast (cha);

			characters [i+3] = cha;
		}
	}


	public void moveBoat() {
		if (boat.isEmpty ())
			return;
		boat.Move ();
		userGUI.status = check_game_over ();
	}

	public void characterIsClicked(MyCharacterController characterCtrl) {
		if (characterCtrl.isOnBoat ()) {
			CoastController whichCoast;
			if (boat.get_to_or_from () == -1) { // to->-1; from->1
				whichCoast = toCoast;
			} else {
				whichCoast = fromCoast;
			}

			boat.GetOffBoat (characterCtrl.getName());
			characterCtrl.moveToPosition (whichCoast.getEmptyPosition ());
			characterCtrl.getOnCoast (whichCoast);
			whichCoast.getOnCoast (characterCtrl);

		} else {									// character on coast
			CoastController whichCoast = characterCtrl.getCoastController ();

			if (boat.getEmptyIndex () == -1) {		// boat is full
				return;
			}

			if (whichCoast.get_to_or_from () != boat.get_to_or_from ())	// boat is not on the side of character
				return;

			whichCoast.getOffCoast(characterCtrl.getName());
			characterCtrl.moveToPosition (boat.getEmptyPosition());
			characterCtrl.getOnBoat (boat);
			boat.GetOnBoat (characterCtrl);
		}
		userGUI.status = check_game_over ();
	}

	int check_game_over() {	// 0->not finish, 1->lose, 2->win
		int from_priest = 0;
		int from_devil = 0;
		int to_priest = 0;
		int to_devil = 0;

		int[] fromCount = fromCoast.getCharacterNum ();
		from_priest += fromCount[0];
		from_devil += fromCount[1];

		int[] toCount = toCoast.getCharacterNum ();
		to_priest += toCount[0];
		to_devil += toCount[1];

		if (to_priest + to_devil == 6)		// win
			return 2;

		int[] boatCount = boat.getCharacterNum ();
		if (boat.get_to_or_from () == -1) {	// boat at toCoast
			to_priest += boatCount[0];
			to_devil += boatCount[1];
		} else {	// boat at fromCoast
			from_priest += boatCount[0];
			from_devil += boatCount[1];
		}
		if (from_priest < from_devil && from_priest > 0) {		// lose
			return 1;
		}
		if (to_priest < to_devil && to_priest > 0) {
			return 1;
		}
		return 0;			// not finish
	}

	public void restart() {
		boat.reset ();
		fromCoast.reset ();
		toCoast.reset ();
		for (int i = 0; i < characters.Length; i++) {
			characters [i].reset ();
		}
	}
}

View部分

  • UserGUI
    這裏主要是調用OnGUI來實現一些圖形界面的顯示效果,以及設置遊戲的restart,開始暫停等功能。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Com.Mygame;

public class UserGUI : MonoBehaviour {
	private UserAction action;
	public int status = 0;
	GUIStyle style;
	GUIStyle buttonStyle;

	void Start() {
		action = Director.getInstance ().currentSceneController as UserAction;

		style = new GUIStyle();
		style.fontSize = 40;
		style.alignment = TextAnchor.MiddleCenter;

		buttonStyle = new GUIStyle("button");
		buttonStyle.fontSize = 30;
	}
	void OnGUI() {
		if (status == 1) {
			GUI.Label(new Rect(Screen.width/2-50, Screen.height/2-85, 100, 50), "Gameover!", style);
			if (GUI.Button(new Rect(Screen.width/2-70, Screen.height/2, 140, 70), "Restart", buttonStyle)) {
				status = 0;
				action.restart ();
			}
		} else if(status == 2) {
			GUI.Label(new Rect(Screen.width/2-50, Screen.height/2-85, 100, 50), "You win!", style);
			if (GUI.Button(new Rect(Screen.width/2-70, Screen.height/2, 140, 70), "Restart", buttonStyle)) {
				status = 0;
				action.restart ();
			}
		}
	}
}
  • ClickGUI
    用於反饋用戶的點擊,並調用SceneController進行響應的。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Com.Mygame;

public class ClickGUI : MonoBehaviour {
	UserAction action;
	MyCharacterController characterController;

	public void setController(MyCharacterController characterCtrl) {
		characterController = characterCtrl;
	}

	void Start() {
		action = Director.getInstance ().currentSceneController as UserAction;
	}

	void OnMouseDown() {
		if (gameObject.name == "boat") {
			action.moveBoat ();
		} else {
			action.characterIsClicked (characterController);
		}
	}
}

運行效果

在這裏插入圖片描述在這裏插入圖片描述

視頻鏈接-lose?
視頻鏈接-win?

  • 最後
    感覺這次作業的編程遊戲難度還是挺大的,所以代碼和設計思路很大程度上都是參考的這篇博客做的,感謝師兄的分享!參考文檔

我的Github代碼傳送門

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章