空间与运动
简答并用程序验证
1. 游戏对象运动的本质
游戏对象运动的本质就是使用矩阵变换,例如平移、旋转、缩放来改变游戏对象的空间属性。
2.请用三种方法以上方法,实现物体的抛物线运动。(如,修改Transform属性,使用向量Vector3的方法…)
方法1:使用重力属性
实现抛物线最直观的实现方法就是将物体抛出去,然后在重力属性的作用下让物体自由下落就可以形成抛物线运动。我们只需要使用脚本给物体一个沿斜上方的初速度就行了。
cure1.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class cure1 : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
this.GetComponent<Rigidbody>().velocity = new Vector3(10, 10, 0);
}
}
实现效果
方法2:修改物体position属性
物体的运动实际上就是物体在游戏每次更新时位置的变化,所以我们只需要在Update中按照抛物线的轨迹更新物体的位置就可以了。首先确定物体水平初速度和竖直初速度,每次按照水平和竖直速度更新物体位置,并且根据重力加速度修改竖直方向速度。
cure2.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class cure2 : MonoBehaviour
{
private float x_v;
private float y_v;
private float g;
// Start is called before the first frame update
void Start()
{
x_v = 10f;
y_v = 10f;
g = 10f;
}
// Update is called once per frame
void Update()
{
y_v -= g * Time.deltaTime;
this.transform.position += new Vector3(Time.deltaTime*x_v, Time.deltaTime*y_v, 0);
}
}
实现效果
方法3:使用Translate
我们可以直接使用Translate来实现物体位置变换,Translate第一个参数就传入物体运动方向及速度(Vector3),速度计算也是上一个方法一样,第二个参数使用Space.World,也就物体相对于世界坐标系运动。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class cure3 : MonoBehaviour
{
private float x_v;
private float y_v;
private float g;
// Start is called before the first frame update
void Start()
{
x_v = 10f;
y_v = 10f;
g = 10f;
}
// Update is called once per frame
void Update()
{
y_v -= g * Time.deltaTime;
transform.Translate(new Vector3(Time.deltaTime * x_v, Time.deltaTime * y_v, 0), Space.World);
}
}
实现效果
方法4:使用Lerp
使用Vector3.Lerp方法也可以模拟出物体运动轨迹并修改物体的position属性。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class cure4 : MonoBehaviour
{
private float x_v;
private float y_v;
private float g;
// Start is called before the first frame update
void Start()
{
x_v = 10f;
y_v = 10f;
g = 10f;
}
// Update is called once per frame
void Update()
{
y_v -= g * Time.deltaTime;
transform.position = Vector3.Lerp(
transform.position, transform.position + new Vector3(Time.deltaTime * x_v, Time.deltaTime * y_v, 0), 1);
}
}
实现效果
从以上四种抛物线实现效果来看,这四种方法的实现效果几乎是一样的。
3.写一个程序,实现一个完整的太阳系, 其他星球围绕太阳的转速必须不一样,且不在一个法平面上。
实现一个太阳系的关键就在于实现一个物体以另一个物体为中心旋转,同时进行自转。我们可以使用RotateAround(Sun.position, axis, speed * Time.deltaTime);使得物体以太阳所在位置为圆形,以axis(一个Vector3向量)为法线,并以speed为速度旋转。要使物体进行自转只需要将RotateAround的第一个参数设置为自己当前的位置就可以了。
Planet.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Planet : MonoBehaviour
{
public Transform Sun;
public float speed = 20;
float ry, rx, rz;
// Use this for initialization
void Start()
{
speed = Random.Range(10, 50);
rx = Random.Range(0, 20);
ry = Random.Range(-1, 1);
rz = Random.Range(-1, 1);
}
// Update is called once per frame
void Update()
{
Vector3 axis = new Vector3(0, 5, rz);
this.transform.RotateAround(this.transform.position, Vector3.up, 2);
this.transform.RotateAround(Sun.position, axis, speed * Time.deltaTime);
}
}
为场景中每一个行星都添加上这个脚本组件,就可以实现所有行星围绕太阳的转动并且同时自转。每个行星的axis参数在Start()运行时由随机数生成,但是生成的随机数范围比较小,所以所有行星围绕太阳转动的平面都不在同一个法平面上,但是相差范围不大,这也符合太阳系的实际情况。旋转的速度也是由随机数生成,所以每个行星沿着轨道运行的速度也各不相同。
为了使得场景更加丰富,我在网上找到了一些行星的贴图素材,这样每一个行星就不只是个单纯的球体了。
为了使游戏场景效果更丰富,我们可以在Camera上添加一个脚本使得玩家可以控制摄像头的移动,使得玩家可以修改游戏的视角,从而可以在不同角度观察太阳系。
Camera_control.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Camera_control : MonoBehaviour
{
public float speed = 20;
// Update is called once per frame
void Update()
{
Vector3 left = new Vector3(-1, 0, 0);
Vector3 right = new Vector3(1, 0, 0);
Vector3 forward = new Vector3(0, -1, 0);
Vector3 back = new Vector3(0, 1, 0);
if (Input.GetKeyDown(KeyCode.A))
{
this.transform.position += left;
}
if (Input.GetKeyDown(KeyCode.D))
{
this.transform.position += right;
}
if (Input.GetKeyDown(KeyCode.S))
{
this.transform.position += back;
}
if (Input.GetKeyDown(KeyCode.W))
{
this.transform.position += forward;
}
}
}
将这个脚本添加到摄像机上之后,玩家就可以通过’w’ ‘a’ ‘s’ 'd’按键来移动摄像机位置。
实现效果
可以看到每个行星都是围绕太阳旋转,并且旋转法平面(设置是给的差距不大)和速度都不同。同时玩家可以通过按下WASD来调整视角。
编程实践(Priests and Devils游戏实现)
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)
- 用表格列出玩家动作表(规则表),注意,动作越少越好
请将游戏中对象做成预制 - 在 GenGameObjects 中创建 长方形、正方形、球 及其色彩代表游戏中的对象。
- 使用 C# 集合类型 有效组织对象
- 整个游戏仅 主摄像机 和 一个 Empty 对象, 其他对象必须代码动态生成!!! 。 整个游戏不许出现 Find 游戏对象, SendMessage 这类突破程序结构的 通讯耦合 语句。 违背本条准则,不给分
- 请使用课件架构图编程,不接受非 MVC 结构程序
- 注意细节,例如:船未靠岸,牧师与魔鬼上下船运动中,均不能接受用户事件!
游戏中对象简介
在这个简单的游戏中涉及到的游戏对象也不是太多,接下来我分类介绍不同游戏对象的功能(整个场景中的对象都是由不同颜色的方块模拟出来的)。
角色:在这个游戏中总共有6个角色对象,其中3个是牧师3个是恶魔。由于在控制逻辑上牧师和恶魔没有区别,所以我将他们作为同一类游戏对象进行管理。
船(Boat):船用于承载角色跨过河流,船上只有两个座位,而且船只能在河中左右移动。
陆地(Land):陆地在这个游戏中就只是简单的场景功能,不具有与用户交互等功能。总共有左右两个陆地对象作为河岸,用于承载牧师和恶魔。
河流(River):河流是位于两块陆地之间的游戏对象,用于承载船对象,本身也不具有与用户交互等功能,也只是个场景而已。
玩家动作表(规则表)
玩家动作 | 动作效果 |
---|---|
点击restart按钮 | 游戏重新开始,所有角色和船的位置重置 |
点击游戏角色(牧师或恶魔) | (前提是船和角色在同一侧)角色如果在船上则上岸,如果在岸上则上船 |
点击船 | 如果船上有乘客则船及其乘客移动到对岸,否则不移动 |
MVC框架简介
在这次任务中我们必须要使用MVC框架来实现我们的游戏。MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。MVC被独特的发展起来用于映射传统的输入、处理和输出功能在一个逻辑的图形化用户界面的结构中。
游戏实现
首先我们创建一个总的Director导演类,这个类可以通过一个抽象的接口访问不同的场记(也就是之后我们要实现的场景控制器)。在getInsatnce函数中,只有第一次调用这个函数时会创建一个新的SSDirector实例,所以这个特性保证SSDirector是单实例的。
public class SSDirector : System.Object
{
private static SSDirector _instance;
public ISceneController currentSceneController { get; set; }
public static SSDirector getInstance()
{
if (_instance == null)
{
_instance = new SSDirector();
}
return _instance;
}
}
我们可以看到SSDirector类中存在一个类型为ISceneController类的场记。接下来我们需要定义一个场记的接口ISceneController,这个接口明确了场记需要实现的功能。Director通过ISceneController规定了每个场记必须实现的功能,而实现了对应接口的类就可以作为场记。在这个游戏中场记需要实现的必须功能就是LoadResources来载入场上所有的资源。
public interface ISceneController
{
void LoadResources();
}
接下来实现Controller作为整个游戏的核心控制器。Controller继承了ISceneController和IUserAction,所以在这个控制器中必须要实现场记的基本功能以及用户接口。在这个控制器中保存了各个游戏对象的控制器,以及用于标志当前船是否在移动的标志boat_moving(这个标志用于禁止船移动过程中中的操作)。Controller的各个函数实现实际上是通过间接调用其他游戏对象的控制器实现的。
public class Controller : MonoBehaviour, ISceneController, IUserAction
{
private GameObject left_land, right_land, river;
private CharacterModel[] MCharacter;
private BoatModel MBoat;
private bool boat_moving;
// Start is called before the first frame update
void Awake()
{
SSDirector director = SSDirector.getInstance();
director.currentSceneController = this;
MCharacter = new CharacterModel[6];
director.currentSceneController.LoadResources();
boat_moving = false;
}
public void LoadResources()
{
Vector3 left_land_pos = new Vector3(-8F, 0, 0);
Vector3 right_land_pos = new Vector3(8F, 0, 0);
Vector3 river_pos = new Vector3(0, -0.5F, 0);
Vector3 boat_pos = new Vector3(4F, 0.25F, 0);
Vector3[] charactor_pos = { new Vector3(5.25F, 1.25F, 0), new Vector3(6.25F, 1.25F, 0), new Vector3(7.25F, 1.25F, 0),
new Vector3(8.25F, 1.25F, 0), new Vector3(9.25F, 1.25F, 0), new Vector3(10.25F, 1.25F, 0) };
left_land = Object.Instantiate(Resources.Load("Land", typeof(GameObject)), left_land_pos, Quaternion.identity, null) as GameObject;
left_land.name = "left_land";
right_land = Object.Instantiate(Resources.Load("Land", typeof(GameObject)), right_land_pos, Quaternion.identity, null) as GameObject;
right_land.name = "right_land";
river = Object.Instantiate(Resources.Load("River", typeof(GameObject)), river_pos, Quaternion.identity, null) as GameObject;
river.name = "river";
MBoat = new BoatModel(boat_pos);
for (int i = 0; i < 6; i++)
{
if (i < 3)
MCharacter[i] = new CharacterModel(0, charactor_pos[i], i);
else
MCharacter[i] = new CharacterModel(1, charactor_pos[i], i);
}
}
public int get_game_situation() // -1:fail 0:not_end 1:success
{
int left_d = 0, left_p = 0, right_d = 0, right_p = 0;
for (int i = 0; i < 6; i++)
{
if(i < 3)
{
if (MCharacter[i].get_side() == -1)
right_d += 1;
else
left_d += 1;
}
else
{
if (MCharacter[i].get_side() == -1)
right_p += 1;
else
left_p += 1;
}
}
if (left_d == 3 && left_p == 3) return 1;
if (((left_d > left_p)&&(left_p!=0))|| ((right_d > right_p)&&(right_p!=0))) return -1;
return 0;
}
public void stop_all()
{
for (int i = 0; i < 6; i++)
MCharacter[i].stop_character();
MBoat.stop_boat();
}
public void enable_all()
{
for (int i = 0; i < 6; i++)
MCharacter[i].enable_character();
MBoat.enable_boat();
}
public void move_boat()
{
Debug.Log("Move boat");
if (MBoat.is_empty())
return;
MBoat.turn_side();
int[] custom_num = MBoat.get_customs();
if (custom_num[0] != -1)
MCharacter[custom_num[0]].turn_side();
if (custom_num[1] != -1)
MCharacter[custom_num[1]].turn_side();
boat_moving = true;
Debug.Log(UserGUI.situation);
}
public void click_character(int character_num)
{
if (MCharacter[character_num].get_whether_on_boat())
to_land(character_num);
else
take_boat(character_num);
}
public void take_boat(int character_num)
{
if (!MBoat.has_empty() || MCharacter[character_num].get_side() != MBoat.get_side()||
MCharacter[character_num].get_whether_on_boat())
return;
Vector3 boat_seat = MBoat.get_seat(character_num);
MCharacter[character_num].take_boat(boat_seat);
}
public void to_land(int character_num)
{
if (!MCharacter[character_num].get_whether_on_boat())
return;
MBoat.clear_seat(character_num);
MCharacter[character_num].to_land();
}
// Update is called once per frame
void Update()
{
if (!boat_moving) return;
int[] custom_num = MBoat.get_customs();
if (custom_num[0] != -1)
MCharacter[custom_num[0]].move_with_boat();
if (custom_num[1] != -1)
MCharacter[custom_num[1]].move_with_boat();
if (MBoat.move_boat())
{
UserGUI.situation = get_game_situation();
if (UserGUI.situation != 0) stop_all();
boat_moving = false;
}
}
public void restart()
{
MBoat.restart();
for (int i = 0; i < 6; i++)
MCharacter[i].restart();
enable_all();
}
}
接下来就是创建控制各个游戏对象的Model。在这个游戏中总共只有四类游戏对象,分别是陆地、河流、船、人物(恶魔和牧师归为一类)。由于陆地和河流在整个游戏中只是一个背景,不存在与用户的交互也不存在特别的效果,所以当Controller创建这几个游戏对象之后就不需要额外的控制器来管理了。因此我们就只需要创建管理人物和船的控制器。
首先我们创建的是控制角色的CharacterModel,其中保存了角色的属性(恶魔或者是牧师)、角色编号(唯一的编号,用于角色定位和标记)、角色是否在船上、角色在河的哪一边以及一些控制角色运动的属性。
public class CharacterModel
{
readonly GameObject character;
readonly int type; // devil:0, priest:1
private int which_side; // right:-1, left:1
private bool on_boat;
readonly int character_num;
readonly Vector3 ori_pos;
private Vector3 dst_pos;
private ClickAction click_action;
public CharacterModel(int Type, Vector3 Ori_pos, int i)
{
ori_pos = Ori_pos;
on_boat = false;
which_side = -1;
type = Type;
character_num = i;
if (Type == 0)
{
character = Object.Instantiate(Resources.Load("Devil", typeof(GameObject)), ori_pos, Quaternion.identity, null) as GameObject;
character.name = "devil" + i.ToString();
}
else
{
character = Object.Instantiate(Resources.Load("Priest", typeof(GameObject)), ori_pos, Quaternion.identity, null) as GameObject;
character.name = "priest" + (i-3).ToString();
}
click_action = character.AddComponent(typeof(ClickAction)) as ClickAction;
click_action.set_character_num(character_num);
}
public void stop_character()
{
click_action.set_moveable(false);
}
public void enable_character()
{
click_action.set_moveable(true);
}
public void turn_side()
{
which_side *= -1;
}
public int get_side()
{
return which_side;
}
public void set_whether_on_boat(bool turn)
{
on_boat = turn;
}
public bool get_whether_on_boat()
{
return on_boat;
}
public void take_boat(Vector3 seat)
{
on_boat = true;
character.transform.position = seat;
}
public void to_land()
{
on_boat = false;
Vector3 curr_pos = ori_pos;
curr_pos.x = curr_pos.x * (float)which_side * -1;
character.transform.position = curr_pos;
}
public void move_with_boat()
{
if (!on_boat) return;
Vector3 move = new Vector3((float)which_side * -20F, 0, 0);
//dst_pos
character.transform.position += move* Time.deltaTime;
//which_side *= -1;
}
public void restart()
{
on_boat = false;
which_side = -1;
character.transform.position = ori_pos;
}
}
现在我们要来创建控制船的控制器,其中包含了船上的乘客编号(对应角色编号)、船在河的哪一边、船上座位的相对位置信息以及一些用于船运动的属性。
public class BoatModel
{
readonly GameObject boat;
private int which_side; // right:-1, left:1
readonly Vector3 ori_pos;
private Vector3 dst_pos;
readonly Vector3[] relative_pos;
private int[] seat;
readonly ClickAction click_action;
public BoatModel(Vector3 boat_pos)
{
which_side = -1;
seat = new int[2];
seat[0] = -1;
seat[1] = -1;
relative_pos = new Vector3[2];
relative_pos[0] = new Vector3(-0.5F, 0.5F, 0);
relative_pos[1] = new Vector3(0.5F, 0.5F, 0);
ori_pos = boat_pos;
boat = Object.Instantiate(Resources.Load("Boat", typeof(GameObject)), boat_pos, Quaternion.identity, null) as GameObject;
boat.name = "boat";
click_action = boat.AddComponent(typeof(ClickAction)) as ClickAction;
}
public void stop_boat()
{
click_action.set_moveable(false);
}
public void enable_boat()
{
click_action.set_moveable(true);
}
public int get_side()
{
return which_side;
}
public void turn_side()
{
which_side*=-1;
}
public bool has_empty()
{
return ((seat[0] == -1) || (seat[1] == -1));
}
public bool is_empty()
{
return ((seat[0] == -1) && (seat[1] == -1));
}
public int[] get_customs()
{
return seat;
}
public Vector3 get_seat(int custom_num)
{
if (seat[0] == -1)
{
seat[0] = custom_num;
return boat.transform.position + relative_pos[0];
}
seat[1] = custom_num;
return boat.transform.position + relative_pos[1];
}
public void clear_seat(int custom_num)
{
if (seat[0] == custom_num)
seat[0] = -1;
else if (seat[1] == custom_num)
seat[1] = -1;
}
public void restart()
{
which_side = -1;
seat = new int[2];
seat[0] = -1;
seat[1] = -1;
boat.transform.position = ori_pos;
}
public bool move_boat()
{
if ((seat[0] == -1) && (seat[1] == -1))
return false;
dst_pos = new Vector3(-4F * (float)which_side, 0.25F, 0);
boat.transform.position = Vector3.MoveTowards(boat.transform.position, dst_pos, 20 * Time.deltaTime);
if (boat.transform.position.x == dst_pos.x) return true;
return false;
}
}
实现了场景的控制器和游戏对象的控制器之后我们要来实现用户与游戏的交互方面。首先我们定义用户与游戏的交互接口IUserAction。根据之前列出的玩家动作表,我们可以知道玩家在游戏中可以使用的操作非常有限,只有重开游戏、移动船和点击角色这几个操作,所以我们在IUserAction接口中定义这几个操作,而在Controller中必须要实现这些操作。
public interface IUserAction
{
void restart();
void move_boat();
void click_character(int character_num);
}
在UserGUI中实现了游戏界面,实际上也就只是实现了一个可以重启的按键以及游戏结束时的提示标签。
public class UserGUI : MonoBehaviour
{
private IUserAction action;
public static int situation = 0;
public GUISkin MY_GUI;
// Start is called before the first frame update
void Start()
{
action = SSDirector.getInstance().currentSceneController as IUserAction;
}
void OnGUI()
{
GUI.skin = MY_GUI;
if (situation == -1)
GUI.Label(new Rect(Screen.width * 7 / 16, Screen.height * (7 / 16),
Screen.width / 8, Screen.height / 8), "You lose!");
else if(situation == 1)
GUI.Label(new Rect(Screen.width * 7 / 16, Screen.height * (7 / 16),
Screen.width / 8, Screen.height / 8), "You win!");
if (GUI.Button(new Rect(Screen.width * 7 / 16, Screen.height / 8,
Screen.width / 8, Screen.height / 8), "Restart"))
{
situation = 0;
Debug.Log("Game restart");
action.restart();
}
}
}
由于船和角色被点击都会触发相应操作,所以需要在每个可以被点击的游戏对象上添加一个可点击的组件。我在ClickAction中实现了游戏对象被点击的事件。当船被点击时就调用move_boat,角色被点击时就调用click_character(character_num),这两个函数都在接口中定义,并且在SceneController中实现了。有了这些接口的使用,使用者就完全不用在意这些函数是如何实现的。同时在ClickAction中还保存了当前角色的编号(如果是船就根本不会用到这个属性),还保存了moveable标志位来决定当前对象是否可被点击,这个属性是用于禁止用户在船或者角色移动时进行点击操作。
public class ClickAction : MonoBehaviour
{
private IUserAction action;
private int character_num;
private bool moveable;
// Start is called before the first frame update
void Start()
{
action = SSDirector.getInstance().currentSceneController as IUserAction;
moveable = true;
}
public void set_character_num(int num)
{
character_num = num;
}
public void set_moveable(bool move)
{
moveable = move;
}
void OnMouseDown()
{
if (!moveable) return;
if (gameObject.name == "boat")
{
action.move_boat();
}
else
{
action.click_character(character_num);
}
}
}
将以上脚本全部挂载在一个Empty游戏对象上然后运行游戏就可以看到游戏效果了。
实现效果
游戏工程的详细内容参见我的github:https://github.com/Eric3778/priests-and-devils,如果有什么不足请大家及时指出,谢谢阅读!