这次的作业是有关空间与运动的简答题和编程题
编程题:
github代码
一、简答题
(1)游戏对象运动的本质是什么?
游戏对象通过C#脚本改变Transform(position、rotation和scale)
(2)请用三种方法以上方法,实现物体的抛物线运动
a.第一种方法比较直观,就是直接改变游戏对象的position属性
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class test : MonoBehaviour
{
public Vector3 v = new Vector3(3, 0, 0);//初速度
public Vector3 g = new Vector3(0, -10, 0);//重力加速度
void Update()
{
this.transform.position = (v * Time.time + 0.5f * g * Time.time * Time.time);
}
}
b.第二种方法是使用translate
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class test : MonoBehaviour
{
public Vector3 v = new Vector3(3, 0, 0);//初速度
public Vector3 g = new Vector3(0, 0, 0);//重力方向速度
private float tmpTime = 0;
// Update is called once per frame
void Update()
{
g.y -= 10 * Time.deltaTime;
transform.Translate(v * Time.deltaTime);
transform.Translate(g * Time.deltaTime);
}
}
c、给物体直接增加重力
在右边的组件中添加刚体
然后在刚体属性中默认勾选了重力选项(Use Gravity):
然后添加初速度,无论用前面三种方法中的哪一种,都可以形成抛物线
(3)太阳系
太阳系的构成有太阳、八大行星(含有月球)。于是我们不难得出游戏对象的树状结构:
这里我将地球和月球归入地月系统。很多人可能会将月球和地球并列,这在游戏对象比较少的时候当然是可以接受(或者说忍受)。但是当游戏对象很多的时候,这种随意安排对象间的“父子”关系将会带来许多缺点,如给读者带来极差的阅读体验和cs代码的混乱。所以这里特意将地月系统单独划出来。
相应的创建sphere后,加上贴图,调整位置,效果如下图所示:
现在我们给太阳系写一个旋转的脚本。注意满足以下要求:
星球围绕太阳的转速必须不一样,且不在一个法平面上
1、创建Transform对象,用于记录各个星体的状态
public Transform Sun;//太阳
public Transform Mercury;//水星
public Transform Venus;//金星
public Transform EarthMoon;//地月系统
……
Start的时候给星体一个初始位置(可以省略,因为已经摆放好了各个星体)
Sun.localPosition = Vector3.zero;
Mercury.localPosition = new Vector3(6, 0, 0);
最后在Update中考虑自转与公转。设立变量a,作为公转轴,以保证不同行星位于不同的法平面上
Vector3 a1 = new Vector3(0, -10, 5);
Vector3 a2 = new Vector3(0, -10, 1);
Vector3 a3 = Vector3.down;
//地球就用水平面作为参照
Vector3 a4 = new Vector3(0, -10, -5);
……
问题(待解决)
这里公转轴的选取很有讲究。我们想实现的效果,肯定是行星围绕太阳转。但是如果你将a1改为(5,10,0),你将会发现水星公转的圆心并不是(0,0,0),这可能和欧拉角和四元数的转换有关
然后设立公转和自转,注意保证公转速度不一样
Sun.Rotate(Vector3.down * 10 * Time.deltaTime);
Mercury.RotateAround(Sun.position, a1, 50 * Time.deltaTime);
Mercury.Rotate(Vector3.down * 10 * Time.deltaTime);
Venus.RotateAround(Sun.position, a2, 30 * Time.deltaTime);
Venus.Rotate(Vector3.up * 5 * Time.deltaTime);
EarthMoon.RotateAround(Sun.position, a3, 20 * Time.deltaTime);
……
注意
地月系统没有自转,就像太阳系不应该有自转一样。他们只是空对象,地球的自转应该交给地球去完成,而不是地月系统一起完成。如果在这里犯错,结果显而易见,月球被迫跟着地球以同样的速度“自转”。月球的这种“自转”更像是月球围着地球公转
注意
有几个有趣的天文现象。如金星的自转方向与其他行星相反、天王星的自转轴是“平躺着的”
Mercury.Rotate(Vector3.down * 10 * Time.deltaTime);
//正常的水星↑
Venus.Rotate(Vector3.up * 5 * Time.deltaTime);
//金星↑
Uranus.Rotate(Vector3.forward * 30 * Time.deltaTime);
//天王星↑
把上述代码拖到太阳系上
再把各个星体对象拖到右侧
地月系统也是类似,但是我们还要注意一个问题
注意
RotateAround作用的是世界坐标。我们看看这一行代码
Moon.RotateAround(Earth.position, a, 10 * Time.deltaTime);
如果这里的position改为localPosition,月球将会将地球在地月系统中的坐标当成地球的世界坐标。因为地球位于地月系统的中央,地球在地月系统中的坐标是(0,0,0),所以月球就围绕太阳转了,变成了行星
进一步思考
此时月球围绕太阳公转的速度是,原本月球围绕地球公转的速度,加上地月系统公转的速度
放一个效果图吧
近距离看一看地月系统,可以明显看到月球围绕地球公转
二、编程题
这次的编程题是牧师与魔鬼。游戏规则很简单,用一艘能承载两个单位的船,将三个牧师与三个魔鬼送到对岸。需要满足的条件是,在两岸和船上的牧师数量都分别不少于魔鬼(法师别送 ),当然没有牧师、只有魔鬼是允许的。
注意
1、列出游戏中提及的事物(Objects)
2、用表格列出玩家动作表(规则表),注意,动作越少越好
3、请将游戏中对象做成预制
4、在 GenGameObjects 中创建长方形、正方形、球及其色彩代表游戏中的对象
5、使用 C# 集合类型有效组织对象
6、整个游戏仅主摄像机和一个 Empty 对象,其他对象必须代码动态生成。整个游戏不许出现 Find 游戏对象, SendMessage 这类突破程序结构的通讯耦合语句
7、请使用课件架构图编程,不接受非 MVC 结构程序
8、注意细节,例如:船未靠岸,牧师与魔鬼上下船运动中,均不能接受用户事件
1、游戏中提及的事物(Objects):Priest(牧师)、Devil(魔鬼)、船(boat)、岸(bank)
2、玩家动作表
动作 | 含义 |
---|---|
点击牧师或魔鬼 | 如果牧师或魔鬼在岸上,且船在被点击的这一侧,且船上位置充足,牧师或魔鬼上船;如果牧师或魔鬼在船上,牧师或魔鬼下船 |
点击船 | 如果船上至少有一个牧师和魔鬼,船到另一侧 |
点击restart | 重新开始游戏 |
3、游戏中的对象都是预制,比如生成water:
(1)MVC
MVC架构包括:
在这次作业中,应该这样理解,模型是预设的游戏对象,需要在代码中指定。控制器提供交互接口和场景控制器。具体到“牧师与魔鬼”游戏中,应该指玩家点击事件的处理,以及游戏对象的运动控制管理等。界面是GUI界面,负责处理input,也就是UserGUI
(2)Model
作业要求不能有预设的对象,所以我们应该在代码中生成游戏对象,我以下面这段代码为例:
if (which_character == "priest")
{
character = Object.Instantiate(Resources.Load("Perfabs/Priest", typeof(GameObject)), Vector3.zero, Quaternion.identity, null) as GameObject;
characterType = 0;
}
else
{
character = Object.Instantiate(Resources.Load("Perfabs/Devil", typeof(GameObject)), Vector3.zero, Quaternion.identity, null) as GameObject;
characterType = 1;
}
这是生成牧师或者魔鬼的函数。然后在FirstController中,就可以调用这些函数,动态生成对象。其他的对象也是类似的。不过牧师与魔鬼的数量不止一个,需要有一个编号的过程:
for (int i = 0; i < 3; i++)
{
MyCharacterController priest = new MyCharacterController("priest");
priest.setName("priest" + i);
priest.setPosition(fromBank.getEmptyPosition());
priest.getOnBank(fromBank);
fromBank.getOnBank(priest);
characters[i] = priest;
}
for (int i = 0; i < 3; i++)
{
MyCharacterController devil = new MyCharacterController("devil");
devil.setName("devil" + i);
devil.setPosition(fromBank.getEmptyPosition());
devil.getOnBank(fromBank);
fromBank.getOnBank(devil);
characters[i + 3] = devil;
}
(3)Controller
控制部分分为两个部分,一部分是FirstController,它负责调用具体的控制器,充当导演的角色。而具体的控制器在Controller中,充当场记的角色。我现在分开概述一下
首先是导演——FirstController。首先FirstController需要有几个场记——具体的Controller:
// 用户界面
UserGUI userGUI;
// 两个河岸的控制器
public BankController fromBank;
public BankController toBank;
// 船的控制器
public BoatController boat;
// 人物控制器
private MyCharacterController[] characters;
然后导演就可以调用场记的函数了,如之前提到的加载水、河岸、船只、人物等:
public void loadResources()
{
// 加载水
GameObject water = Instantiate(Resources.Load("Perfabs/Water", typeof(GameObject)), new Vector3(0, 0.5F, 0), Quaternion.identity, null) as GameObject;
water.name = "water";
// 加载河岸
fromBank = new BankController("from");
toBank = new BankController("to");
// 加载船只
boat = new BoatController();
// 加载人物
for (int i = 0; i < 3; i++)
{
MyCharacterController priest = new MyCharacterController("priest");
priest.setName("priest" + i);
priest.setPosition(fromBank.getEmptyPosition());
priest.getOnBank(fromBank);
fromBank.getOnBank(priest);
characters[i] = priest;
}
for (int i = 0; i < 3; i++)
{
MyCharacterController devil = new MyCharacterController("devil");
devil.setName("devil" + i);
devil.setPosition(fromBank.getEmptyPosition());
devil.getOnBank(fromBank);
fromBank.getOnBank(devil);
characters[i + 3] = devil;
}
}
还有重新开始的函数:
public void restart()
{
boat.reset();
fromBank.reset();
toBank.reset();
for (int i = 0; i < characters.Length; i++)
{
characters[i].reset();
}
}
比如boat.reset();
就是调用了boat控制器(场记)
不同场记对应不同的控制器,一共有:移动控制器、人物控制器、河岸控制器、船只控制器等。他们分别只负责相应对象的行为。代码较长,这里就不展示了
(4)View
用户交互界面:
void OnGUI()
{
// 游戏进行中界面
if(state==0)
{
GUI.Label(new Rect(Screen.width / 2 - 50, Screen.height / 2 - 80, 100, 50), "绿色方块表示牧师,红色球表示魔鬼", style2);
}
// 游戏失败界面
else if (state == 1)
{
GUI.Label(new Rect(Screen.width / 2 - 50, Screen.height / 2 - 80, 100, 50), "Gameover", style1);
if (GUI.Button(new Rect(Screen.width / 2 - 60, Screen.height / 2, 120, 50), "Restart", buttonStyle))
{
state = 0;
action.restart();
}
}
// 游戏成功界面
else if (state == 2)
{
GUI.Label(new Rect(Screen.width / 2 - 50, Screen.height / 2 - 80, 100, 50), "Win", style1);
if (GUI.Button(new Rect(Screen.width / 2 - 60, Screen.height / 2, 120, 50), "Restart", buttonStyle))
{
state = 0;
action.restart();
}
}
}
完整代码地址:[github]