一、简答并用程序验证:
1.1 游戏对象运动的本质是什么?
- 游戏对象通过改变Position来改变位置,通过改变Rotation欧拉角来实现旋转,通过改变Scale来实现形状放缩,所以游戏对象运动的本质是Position,Rotation,Scale三个属性的变化。
1.2 请用三种方法以上方法,实现物体的抛物线运动
- 1.使用Transform属性:
public class Move : MonoBehaviour
{
public float s_s = 20;//start speed
private float s_x;// start x speed
private float s_y;//start y speed
private int g = 10;
// Start is called before the first frame update
void Start()
{
s_x = s_s;
s_y = 0;
}
// Update is called once per frame
void Update()
{
this.transform.position += Vector3.down * Time.deltaTime * s_y;
this.transform.position += Vector3.right * Time.deltaTime * s_x;
s_y += g * Time.deltaTime;
}
}
- 使用Vector3实现抛物线:
public class Move2 : MonoBehaviour
{
public float s_s = 20;//start speed
private float s_x;// start x speed
private float s_y;//start y speed
private int g = 10;
// Start is called before the first frame update
void Start()
{
s_x = s_s;
s_y = 0;
}
// Update is called once per frame
void Update()
{
Vector3 change_vector = new Vector3(Time.deltaTime * s_x, -Time.deltaTime * s_y, 0);
this.transform.position += change_vector;
s_y += g * Time.deltaTime;
}
}
- 使用Translate函数实现:
public class Move3 : MonoBehaviour
{
public float s_s = 20;//start speed
private float s_x;// start x speed
private float s_y;//start y speed
private int g = 10;
// Start is called before the first frame update
void Start()
{
s_x = s_s;
s_y = 0;
}
// Update is called once per frame
void Update()
{
Vector3 change_vector = new Vector3(Time.deltaTime * s_x, -Time.deltaTime * s_y, 0);
this.transform.Translate(change_vector);
s_y += g * Time.deltaTime;
}
}
1.3 写一个程序,实现一个完整的太阳系, 其他星球围绕太阳的转速必须不一样,且不在一个法平面上
为了实现太阳系,我们首先查阅太阳系各个行星之间的属性:
因为按照真实比例进行设定的话,肉眼几乎看不到比较小的星球,所以进行一定合适的放缩和调整后大致关系如下(已经改的不像太阳系了qaq)
行星 | 轨道半长轴 | 轨道速度 | 黄道面倾斜 | 自转周期 | 行星半径 |
---|---|---|---|---|---|
水星 | 5 | 10 | 7° | 50 | 0.6 |
金星 | 8 | 7 | 3°23’ | 200 | 0.95 |
地球 | 11 | 6 | 重合 | 3 | 1 |
火星 | 14 | 5 | 1°51’ | 3 | 0.7 |
木星 | 17 | 3 | 1°18’ | 1 | 2.5 |
土星 | 20 | 2 | 2°29’ | 1 | 2 |
天王星 | 23 | 1.2 | 0°46’ | 2 | 1.5 |
海王星 | 26 | 1 | 1°46’ | 2 | 1.2 |
月球 | 1 | 0.2 | 5°9’ | 200 | 0.3 |
之后首先进行星球的创建,需要创建的有:
- 空GameObject对象:用于搭载脚本实现整体的模拟运动;
- sun :太阳
- Mercury:水星
- Venus:金星
- Earth:地球
- Mars:火星
- Jupiter:木星
- Saturn:土星
- Urranus:天王星
- .Neptune:海王星
- Moon:月球 (Ps:作为地球的子对象创建)
给对象设置初始的位置以及形状,按照上表中给出的轨道半轴长和行星半径设置,轨道半轴长决定Position的x,行星半径决定Scale的 x = y = z 的数值:
如:水星的轨道半轴长为5,行星半径为0.6则在unity中的Transform设置为:
然后给每个行星加上材质包,选取自己喜欢的图片即可。
完成后开始编写行星运行的函数Sun_Route:
这里我在函数中的start中又定义了一次各个行星的Position和Scale,所以其实不需要上一步中的手动定义每个物体的Transform,但是为了之后步骤中的加入TrailRender后显示的轨迹更好看一点所以最好还是手动设置一下;
首先定义我们要用到的Transform:
public Transform Sun;
public Transform Mercury;
public Transform Venus;
public Transform Earth;
public Transform Mars;
public Transform Jupiter;
public Transform Saturn;
public Transform Uranus;
public Transform Neptune;
public Transform Moon;
在start中设定各个行星的位置和形状:
//根据轨道半长轴进行一定的比例调整后进行距离设置
Sun.position = Vector3.zero;
Mercury.position = new Vector3(5, 0, 0);
Venus.position = new Vector3(8, 0, 0);
Earth.position = new Vector3(11, 0, 0);
Mars.position = new Vector3(14, 0, 0);
Jupiter.position = new Vector3(17, 0, 0);
Saturn.position = new Vector3(20, 0, 0);
Uranus.position = new Vector3(23, 0, 0);
Neptune.position = new Vector3(26, 0, 0);
Moon.position = new Vector3(12, 0, 0);
//根据行星大小比例调整后的形状
Sun.transform.localScale = new Vector3(5, 5, 5);
Mercury.transform.localScale = new Vector3(0.6f, 0.6f, 0.6f);
Venus.transform.localScale = new Vector3(0.95f, 0.95f, 0.95f);
Earth.transform.localScale = new Vector3(1, 1, 1);
Mars.transform.localScale = new Vector3(0.7f, 0.7f, 0.7f);
Jupiter.transform.localScale = new Vector3(2.5f, 2.5f, 2.5f);
Saturn.transform.localScale = new Vector3(2f, 2f, 2f);
Uranus.transform.localScale = new Vector3(1.5f, 1.5f, 1.5f);
Neptune.transform.localScale = new Vector3(1.2f, 1.2f, 1.2f);
Moon.transform.localScale = new Vector3(0.3f, 0.3f, 0.3f);
之后再update中实现环绕运动和自转,使用RotateAround函数来实现公转,三个参数分别为旋转的轴心点,旋转的环绕轴的方向,旋转速度,Rotate函数来实现自转,参数为自转速度即自转环绕轴方向:
void Update()
{
//轨道速度决定每个时间片位移,黄道倾斜决定欧拉角
Mercury.RotateAround(Sun.position, new Vector3(0,100,37),100*Time.deltaTime);
Mercury.Rotate(new Vector3(7, 100, 0) * 2 * Time.deltaTime);
Venus.RotateAround(Sun.position, new Vector3(0,100,15),70*Time.deltaTime);
Venus.Rotate(new Vector3(3, 100, 0) * 1 * Time.deltaTime);
Earth.RotateAround(Sun.position, new Vector3(0,1,0),60 * Time.deltaTime);
Earth.Rotate( Vector3.up * 3 * Time.deltaTime);
Moon.transform.RotateAround(Earth.position, Vector3.up, 200 * Time.deltaTime);
Moon.Rotate(Vector3.up * 1 * Time.deltaTime);//question
Mars.RotateAround(Sun.position, new Vector3(0, 100, 10), 50 * Time.deltaTime);
Mars.Rotate(new Vector3(0, 100, 10) * 2 * Time.deltaTime);
Jupiter.RotateAround(Sun.position, new Vector3(0, 100, 25), 30 * Time.deltaTime);
Jupiter.Rotate(new Vector3(0, 100, 25) * 3 * Time.deltaTime);
Saturn.RotateAround(Sun.position, new Vector3(0, 100, 13), 20 * Time.deltaTime);
Saturn.Rotate(new Vector3(0, 100, 13) * 3 * Time.deltaTime);
Uranus.RotateAround(Sun.position, new Vector3(0, 100, 7), 15 * Time.deltaTime);
Uranus.Rotate(new Vector3(0, 100, 7) * 2 * Time.deltaTime);
Neptune.RotateAround(Sun.position, new Vector3(0, 100, 30), 10 * Time.deltaTime);
Neptune.Rotate(new Vector3(0, 100, 30) * 5 * Time.deltaTime);
}
这样我们就完成了旋转代码的编写,将代码拖入空GameObject,并在对应的Transform中对应拖入我们实现的星球:
此时直接运行即可看到简单的模拟太阳系。
美化
但是此时的模拟太阳系比较抽象,而且我们并不能知道它的轨道轨迹,所以我们可以通过为每一个星球增加TailRender来实现轨迹的显示:
使用Component ——》Effects——》Trail Renderer来为八大行星增加尾迹功能,并可通过设置宽度和材质来更换颜色,并可通过增加Time来设定轨迹的显示存活时间
之后更换一下背景,用纯黑图来制作一个天空盒,并更改Window——》Rendering——》light——》Environment——》skybox 来设置为你自己的天空盒,这样就有点在宇宙中的感觉
你还可以通过对你太阳的素材贴图进行shader设置,选择Emission选项来令太阳更像“ 太阳 ”。
完成后我们就可以看到一个“美丽”的太阳系了!
二、编程实践:牧师与魔鬼
2.1题目要求:
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!
2.2 MVC架构:
- 模型(Model):数据对象及关系
- 游戏中的每一个GameObject都是一个Model,每个Model收到对应的Controller的控制,已有Model:Angel(牧师),Devil(魔鬼),Land(岛屿),boat(船)
- 控制器(Controller):接受用户事件,控制模型变化
- 顶层的控制器为Director,只有一个函数getinstance()来保证游戏中只有一个实例运行。
- 第二层的是场景资源加载控制器接口(IsceneController)和用户行为控制器接口(IUserAction),用来加载场景和定义用户行为;
- 第三层是场景控制器(FirstController),继承了上一层的两个接口,控制着场景的加载,物体的移动等功能的实现
- 用户视图(View):显示模型,将人机交互事件交给控制器处理
- 为游戏对象添加ClickUser和UserGUi两个脚本,来进行与用户行为的交互;
2.3 游戏事物:
1. 牧师(Angel)
2. 魔鬼(Devil)
3. 船(boat)
4. 岛屿(land)
2.4 玩家规则表:
事件 | 条件 | 结果 |
---|---|---|
点击人物 | 目的地有空位 | 移动到目的地(船或岛屿) |
点击船 | 船上有角色 | 移动船到另一岛屿 |
点击reset | NULL | 重置游戏 |
2.5 具体实现
在具体实现中,根据我们设定的MVC架构模型来从顶层向底层开始编写:
Director:
首先是Director类,需要实现的是保持唯一实例:
public class Director : System.Object
{
private static Director _instance;
public IsceneController currentISceneController { get; set; }
//to keep instance
public static Director getInstance()
{
if(_instance == null)
{
_instance = new Director();
}
return _instance;
}
}
Director作为最顶层的控制器,主要作用是获得游戏运行时的唯一实例,这样下层的类中,可以通过调用Direct的getinstance函数来获得当前的SceneController来进行类之间的通信和沟通例如在进行reset函数中,使用getinstance来进行重置:
(Director.getInstance().currentISceneController as FirstController).origin_land.get_on_Land(this);
IsceneController:
public interface IsceneController
{
void loadscenesource();
}
IsceneController类用来定义资源加载函数,用于在游戏开始时,进行游戏对象的加载;
IUserAction:
public interface IUserAction //User action
{
void Go();
void replay();
void cha_move(CharactererController cha);
}
IUserAction类用来定义用户动作,定义了船的运行函数Go,用户运动函数cha_move和重置函数replay;
LandController:
public class LandController
{
GameObject Land;
Vector3 origin = new Vector3(9, 0, 0);//start land place
Vector3 destination = new Vector3(-9, 0, 0);// end land place
Vector3[] Landspaces; // land space vector
int[] positions;// record empty space
CharactererController[] passenger;
bool land_states;
......
}
LandController类定义了Land的位置,状态,land上的passenger位置状态等信息,以及一系列Get_on_Land、check_empty等函数,用于在子类中,可以直接调用对应的函数来实现功能;
主要要实现的函数有:
- LandController构造函数:用于初始化位置数组和land_states;
- get_empty_land_place:获取land上的空位置;
- get_empty_land_position: 获取空位置的Vector;
- get_off_Land: 将人物从Land中删除;
- get_on_Land:加人物加入到Land中;
- Reset: 重置Land的属性,用于Reset;
BoatController:
public class BoatController
{
GameObject Boat;
readonly Vector3[] boat_space;
int[] boat_space_signal;
Move move;
readonly Vector3[] origin_boat_seat_space;
readonly Vector3[] dest_boat_seat_space;
bool states;//true = go; false = return
CharactererController[] passenger = new CharactererController[2];
......
}
BoatController类用于定义船的位置,状态,和move脚本等属性,通过向船添加Move脚本实现船的移动
主要函数:
- BoatController构造函数:初始化船的位置和状态信息,以及乘客函数,加载船的模型,挂载Move脚本;
- get_i_passenger:获取船上的某一个乘客;
- check_no_full:检查船上是否非空,用于判定是否可以开船;
- check_status:返回穿的status,用于判定在哪一岸;
- Move:用于移动船;
- find_empty_seat_Vector:用于找到船上的空位Vector;
- setposition:重置船的位置,用于reset;
- get_off_boat:乘客下船;
- get_on_boat:乘客上船;
- Reset:重置穿的位置和船上乘客和状态
CharactererController:
public class CharactererController
{
GameObject character;
bool character_type;//0->priest;1->devil
bool on_boat;//false ->on land; true-> on boat
Move move;//character's Move component
int id;//identify every character
bool which_land;//false->origin_land; true-> dest_land
ClickUser click;
CharacttererController用于定义用户属性,和用户的重载,以及用户的移动等函数,并为用户添加move和click脚本
主要函数:
- CharactererController构造函数:定义用户属性,并实例化预设;
- setposition:重置角色位置,用于reset;
- get_on_boat:角色上船;
- get_off_boat:角色下船;
- Move_to:角色移动函数,用于上船等;
- Reset:重置函数;
- 一系列返回角色属性的return 函数;
Move:
public class Move : MonoBehaviour
{
readonly float move_speed = 20;
int moving_status;
Vector3 dest;
Vector3 middle;
作为整体的move脚本,通过setDestination来设定目的地和相关状态,之后在update中根据状态进行移动;
主要函数:
- setDestination:设定目的地和移动状态;
- Update:Monobehavior的自带函数,用于更新位置;
FirstController:
public class FirstController : MonoBehaviour , IsceneController, IUserAction
{
UserGUI userGUI;
public LandController origin_land;
public LandController dest_land;
public BoatController boat;
private CharactererController[] characters;
FirstController 继承了IsceneController和IUser的接口,实现loadscenesource和用户活动函数,以及进行游戏结束的判断;
主要函数:
- Awake: 在唤醒阶段设定整体的Director的初始化以及资源的挂载,是整个游戏的初始化函数(该函数被挂载到空Gameobject上)
- loadscenesource:资源加载函数,实现water、 land、boat和character的实例化
- Go:实现船的移动,调用Boatontroller类中的Move函数实现;
- replay:重置游戏函数,设置boat,land,character属性初始化;
- GameOver:检查游戏是否结束;
- cha_move:实现鼠标点击角色的移动和用户上船下船状态的设置,调用BoatController和LandController以及CharactererController类中的对应函数进行处理;
UserGUI:
public class UserGUI : MonoBehaviour
{
private IUserAction action;
public int status = 0;
GUIStyle style1;
GUIStyle style2;
GUIStyle style3;
public int TotalTime = 120;
// Start is called before the first frame update
void Start()
{
action = Director.getInstance().currentISceneController as IUserAction;
style1 = new GUIStyle();
style1.fontSize = 40;
style1.alignment = TextAnchor.MiddleCenter;
style2 = new GUIStyle("button");
style2.fontSize = 30;
style3 = new GUIStyle();
style3.fontSize = 30;
StartCoroutine(CountDown());
}
IEnumerator CountDown()
{
while (TotalTime > 0)
{
yield return new WaitForSeconds(1);
TotalTime--;
}
}
private void OnGUI()
{
GUI.Label(new Rect(Screen.width / 2 +450, Screen.height / 2 - 280, 100, 50), "Time: "+ TotalTime, style3);
GUI.Label(new Rect(Screen.width / 2 - 50, Screen.height / 2 - 250, 100, 50), "Priests and Devils", style1);
if (GUI.Button(new Rect(Screen.width / 2 - 70, Screen.height / 2-180, 140, 70), "Restart", style2))
{
TotalTime = 120;
status = 0;
action.replay();
}
if (status == 1)
{
GUI.Label(new Rect(Screen.width / 2 - 50, Screen.height / 2 - 100, 100, 50), "You Die", style1);
}
else if (status == 2)
{
GUI.Label(new Rect(Screen.width / 2 - 50, Screen.height / 2 - 100, 100, 50), "You Win!", style1);
}else if (status == 3)
{
GUI.Label(new Rect(Screen.width / 2 - 50, Screen.height / 2 - 100, 100, 50), "Time over~", style1);
}
}
}
实现游戏标题、reset按钮以及定时器的加载
ClickUesr:
public class ClickUser : MonoBehaviour
{
IUserAction action;
CharactererController character;
public void setController(CharactererController cha)
{
character = cha;
}
// Start is called before the first frame update
void Start()
{
action = Director.getInstance().currentISceneController as IUserAction;
}
void OnMouseDown()
{
if(gameObject.name == "boat")
{
Debug.Log("Go!");
action.Go();
}
else
{
Debug.Log("click!");
action.cha_move(character);
}
}
}
ClickUser用于处理用户行为的反应,根据对应的点击对象,选择执行Go或cha_move函数。
2.6 调用流程
很多同学可能看了更高的MVC架构以及代码之后,对于整体程序结构还不是特别理解,这里整体介绍一下程序执行流程:
- 首先整个程序初始只有一个空的Gameobject组成,并将FirstController脚本挂载在空Gameobject上,当游戏开始后,通过调用Awake函数中的loadscenesource函数来实现预设的实例化,加载其他的游戏对象;
- 游戏对象的实例化在对应的Controller中执行,对应的函数也通过调用控制器函数进行实现,为每个游戏对象挂载的实际函数只有两个:
left
- 当用户点击角色后,首先触发ClickUser中的Onmousedown,并调用ClickUser中的IUserAction接口的GO或cha_move函数,之后接口会从它的具体实现,也就是FirstController控制器中执行具体的函数操作:
- 之后在cha_move函数中会调用对应的每个角色控制器中的函数来实现角色位置、状态等的改变。
- 最后每个角色控制器调用Move脚本,实现角色在游戏中的移动。
2.7 踩坑提醒:
- 建议先实现每个角色控制器的基础部分,不一定要全部完成,之后再完成具体函数的过程中完善每个角色控制器的具体函数,因为船、人物、岛屿可能有互相之间的调用;
- 如果用网上下载的模型,记得查看预设模型的属性,主要检查 有没有添加box碰撞 和 static属性有没有关闭(PS:出现只有刚体碰撞框架移动而物体图像不动建议先看一下预设的static是否关闭)
- Resources.Load需要将预设放到 Assets/Resources/***,对应的目录下,否则无法实现加载;
2.8 结果演示:
动图:
三、使用向量与变换,实现并扩展 Tranform 提供的方法,如 Rotate、RotateAround 等
3.1 实现Rotate:
通过Quaternion.AngleAxis来获取旋转,之后使用原有状态乘以该旋转即可
void Rotate(Transform t, Vector3 axis, float angle)
{
var rotate = Quaternion.AngleAxis(angle, axis);
t.position *= rotate;
t.rotation *= rotate;
}
3.2 实现RotateAround:
通过Quaternion.AngleAxis来获取旋转,之后通过旋转获得并加上旋转后的位移
void RotateAround(Transform t, Vector3 center, Vector3 axis, float angle)
{
var rotate = Quaternion.AngleAxis(angle, axis);
var direction = position - center;
direction *= rotate;
t.position = center + direction;
t.rotation *= rotate ;
}