3D游戏编程与设计——游戏对象与图形基础章节作业与练习
自学资源
- 结构类型
结构类型(“structure type”或“struct type”)是一种可封装数据和相关功能的值类型 。
由于结构类型具有值语义,因此建议定义不可变的 结构类型。
示例:
public struct Coords
{
public Coords(double x, double y)
{
X = x;
Y = y;
}
public double X { get; }
public double Y { get; }
public override string ToString() => $"({X}, {Y})";
}
- 可以使用 readonly 修饰符来声明结构类型不可变
- readonly 结构的所有数据成员都必须是只读的
- 不能声明无参数构造函数。
- 不能在声明实例字段或属性时对它们进行初始化。
- 结构类型的构造函数必须初始化该类型的所有实例字段。
- 结构类型不能从其他类或结构类型继承,也不能作为类的基础类型。 但是,结构类型可以实现接口。
- 不能在结构类型中声明终结器。
- 可以在结构类型的声明中使用 ref 修饰符。 ref 结构类型的实例在堆栈上分配,并且不能转义到托管堆。
- 枚举类型
枚举类型 是由基础整型数值类型的一组命名常量定义的值类型。
enum Season
{
Spring,
Summer,
Autumn,
Winter
}
默认情况下,枚举成员的关联常数值为类型 int;它们从零开始,并按定义文本顺序递增 1。 可以显式指定任何其他整数数值类型作为枚举类型的基础类型。
enum ErrorCode : ushort
{
None = 0,
Unknown = 1,
ConnectionLost = 100,
OutlierReading = 200
}
- 不能在枚举类型的定义内定义方法。若要向枚举类型添加功能,请创建扩展方法。
- 对于任何枚举类型,枚举类型与其基础整型类型之间存在显式转换。
- const关键字
使用 const 关键字来声明某个常量字段或常量局部变量。 常量字段和常量局部变量不是变量并且不能修改。
const int X = 0;
public const double GravitationalConstant = 6.673e-11;
private const string ProductName = "Visual C#";
作业内容
1、基本操作演练【建议做】
- 下载 Fantasy Skybox FREE, 构建自己的游戏场景
主要分为天空盒与地图的制作。
天空盒的制作:
创建一个文件夹为SkyBox,创建一个Material,将Shader改为Skybox/6 Sided或Cubemap并选择刚刚下载的素材中的某一个素材为原料。
然后将该SkyBox拖入到Window-Rendering-Lighting-Environment-Skybox Material
中。
地图的制作:
在对象栏右击->3D Object->Terrain,新建一个地图对象。
使用Terrain的各项工具绘制地图, 包括造山,造草,添加细节等等。
将下载来的素材中的树和草等细节添加到图层中:
整体效果:
- 写一个简单的总结,总结游戏对象的使用
- Camera:是游戏的眼镜,通过Camera来观察游戏世界。
- Light:光源,用来照明或者添加阴影。
- Empty空对象:作为载体,挂载游戏脚本或成为其他对象的父对象等用途。
- Cube等3D object:搭建游戏世界的组成元素,通过设置其Transform等属性来变换它们的位置、形态等。
- Terrain等:组成元素,又是编辑工具,例如Terrain本身是地图,然后又附带了绘制地图的各项工具(造山、造草等)。
2、编程实践
- 牧师与魔鬼 动作分离版
- 【2019开始的新要求】:设计一个裁判类,当游戏达到结束条件时,通知场景控制器游戏结束
项目要求:
- 使用专用的对象来管理运动。在本游戏中的运动即对象的移动,现在对象的移动由动作管理器接手,而不是上一个版本的MoveController,现在不再需要为每个对象都加上Move.cs。
- 设计一个裁判类,当游戏达到结束条件时,通知场景控制器游戏结束。游戏胜利与失败的条件均由JudgeController接管,FirstController新增处理JudgeController反馈的JudgeCallback函数,而取消了Check函数。
优点:代码结构更健全,方便添加新动作。
项目结构:
- 在上版本的MVC结构下,添加Actions,上版本中的MoveController和Move可以去除,由CCActionManager等来管理相关动作。
- 游戏中所有的移动(动作)都与FirstController分离开来,FirstController通过调用CCActionManager的函数来触发动作。
- 判断游戏状态的功能从FirstController中分离开来,引入裁判JudgeController来处理游戏状态事件。
- Scripts/Actions
- Scripts/Controllers
- Scripts/Models
- Scripts/Views
代码详解:
Actions:
ISSActionCallback.cs
ISSActionCallback是向其他函数通信的接口,当一个动作有了结果之后需要向另一个函数传递信息,可通过SSActionEvent回调。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum SSActionEventType : int { Started, Competed }
public interface ISSActionCallback
{
//回调函数
void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competed,int intParam = 0,string strParam = null,Object objectParam = null);
}
SSAction.cs
SSAction是动作类的基类。gameObject为动作作用的实体对象。callback是回调接口,当动作类需要向别的类传递信息时,就通过ISSActionCallback接口来实现。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SSAction : ScriptableObject
{
public bool enable = true;
public bool destroy = false;
public GameObject gameObject { get; set; }
public Transform transform { get; set; }
public ISSActionCallback callback { get; set; }
protected SSAction()
{
}
public virtual void Start()
{
throw new System.NotImplementedException();
}
public virtual void Update()
{
throw new System.NotImplementedException();
}
}
SSActionManager.cs
SSActionManager是动作管理类的基类,作为动作生成、运行与销毁的管理者。
actions存储正在运行中的动作。
waitingAdd存储即将运行的动作。
waitingDelete存储即将被删除的动作。
Update()每次会将waitingAdd中的动作加入到actions当中,然后遍历运行actions中的动作,如果动作已经结束,则加入到waitingDelete中,最后将waitingDelete中的动作删除并销毁。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SSActionManager : MonoBehaviour
{
// 动作集,以字典形式存在
private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();
// 等待被加入的动作队列(动作即将开始)
private List<SSAction> waitingAdd = new List<SSAction>();
// 等待被删除的动作队列(动作已完成)
private List<int> waitingDelete = new List<int>();
protected void Update()
{
// 将waitingAdd中的动作保存
foreach (SSAction ac in waitingAdd)
actions[ac.GetInstanceID()] = ac;
waitingAdd.Clear();
// 运行被保存的事件
foreach (KeyValuePair<int, SSAction> kv in actions)
{
SSAction ac = kv.Value;
if (ac.destroy)
{
waitingDelete.Add(ac.GetInstanceID());
}
else if (ac.enable)
{
ac.Update();
}
}
// 销毁waitingDelete中的动作
foreach (int key in waitingDelete)
{
SSAction ac = actions[key];
actions.Remove(key);
Destroy(ac);
}
waitingDelete.Clear();
}
// 准备运行一个动作,将动作初始化,并加入到waitingAdd
public void RunAction(GameObject gameObject, SSAction action, ISSActionCallback manager)
{
action.gameObject = gameObject;
action.transform = gameObject.transform;
action.callback = manager;
waitingAdd.Add(action);
action.Start();
}
protected void Start()
{
}
}
CCMoveToAction.cs
CCMoveToAction是移动动作类,将物体以一定速度移动到目的地。
GetSSAction是生成目标SSAction的函数,使用工厂模式。
每次调用Update()会使得对象向目的地运动一段距离。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CCMoveToAction : SSAction
{
// 目的地
public Vector3 target;
// 速度
public float speed;
private CCMoveToAction()
{
}
// 生产函数(工厂模式)
public static CCMoveToAction GetSSAction(Vector3 target, float speed)
{
CCMoveToAction action = ScriptableObject.CreateInstance<CCMoveToAction>();
action.target = target;
action.speed = speed;
return action;
}
public override void Start()
{
}
public override void Update()
{
// 判断是否符合移动条件
if (this.gameObject == null || this.transform.localPosition == target)
{
this.destroy = true;
this.callback.SSActionEvent(this);
return;
}
// 移动
this.transform.localPosition = Vector3.MoveTowards(this.transform.localPosition, target, speed * Time.deltaTime);
}
}
CCSequenceAction.cs
CCSequenceAction是组合动作类,包含一系列要进行的动作,并按顺序运行这些动作。
SSActionEvent是回调函数,因为CCSequenceAction是组合动作,因此需要获得每个动作执行状态的信息,当一个动作已经完成,就需要执行下一个动作,或者所有动作完成时销毁自己。
因此当CCSequenceAction中有一个动作完成时,就会调用SSActionEvent通知CCSequenceAction,如果仍有需要执行的动作,则执行下一个动作,否则所有动作已完成,需要销毁自己,即调用callback.SSActionEvent通知其他类来处理。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CCSequenceAction : SSAction, ISSActionCallback
{
// 动作序列
public List<SSAction> sequence;
// 重复次数
public int repeat = -1;
// 动作开始指针
public int start = 0;
// 生产函数(工厂模式)
public static CCSequenceAction GetSSAction(int repeat, int start, List<SSAction> sequence)
{
CCSequenceAction action = ScriptableObject.CreateInstance<CCSequenceAction>();
action.repeat = repeat;
action.start = start;
action.sequence = sequence;
return action;
}
// 对序列中的动作进行初始化
public override void Start()
{
foreach (SSAction action in sequence)
{
action.gameObject = this.gameObject;
action.transform = this.transform;
action.callback = this;
action.Start();
}
}
// 运行序列中的动作
public override void Update()
{
if (sequence.Count == 0)
return;
if (start < sequence.Count)
{
sequence[start].Update();
}
}
// 回调处理,当有动作完成时触发
public void SSActionEvent(SSAction source,
SSActionEventType events = SSActionEventType.Competed,
int Param = 0,
string strParam = null,
Object objectParam = null)
{
source.destroy = false;
this.start++;
if (this.start >= sequence.Count)
{
this.start = 0;
if (repeat > 0)
repeat--;
if (repeat == 0)
{
this.destroy = true;
this.callback.SSActionEvent(this);
}
}
}
void OnDestroy()
{
}
}
CCActionManager.cs
CCActionManager是动作管理者。
isMoving标识当前是否正在运动,本游戏当有物体在运动时不允许用户产生其他操作(除了重开),因此需要使用isMoving来记下当前状态。
moveBoatAction是船移动类,因为船只需要横向移动,因此为单一动作,使用CCMoveToAction。
moveRoleAction是人移动类,因为人需要在两个方向上移动,是组合动作,因此使用CCSequenceAction。
controller为当前与动作管理者关联的控制器
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CCActionManager : SSActionManager, ISSActionCallback
{
// 是否正在运动
private bool isMoving = false;
// 船移动动作类
public CCMoveToAction moveBoatAction;
// 人移动动作类(需要组合)
public CCSequenceAction moveRoleAction;
// 控制器
public FirstController controller;
protected new void Start()
{
controller = (FirstController)SSDirector.GetInstance().CurrentSenceController;
controller.actionManager = this;
}
public bool IsMoving()
{
return isMoving;
}
// 移动船
public void MoveBoat(GameObject boat, Vector3 target, float speed)
{
if (isMoving)
return;
isMoving = true;
moveBoatAction = CCMoveToAction.GetSSAction(target, speed);
this.RunAction(boat, moveBoatAction, this);
}
// 移动人
public void MoveRole(GameObject role, Vector3 mid_destination, Vector3 destination, int speed)
{
if (isMoving)
return;
isMoving = true;
moveRoleAction = CCSequenceAction.GetSSAction(0, 0, new List<SSAction> { CCMoveToAction.GetSSAction(mid_destination, speed), CCMoveToAction.GetSSAction(destination, speed) });
this.RunAction(role, moveRoleAction, this);
}
// 回调函数
public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competed,int intParam = 0, string strParam = null, Object objectParam = null)
{
isMoving = false;
}
}
Controllers:
JudgeController.cs
JudgeController是裁判类,其会在每一帧判断当前游戏是否已经结束,如果已经结束则通过mainController.JudgeCallback()通知主控制器处理。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class JudgeController : MonoBehaviour
{
public FirstController mainController;
public LandModel leftLandModel;
public LandModel rightLandModel;
public BoatModel boatModel;
void Start()
{
mainController = (FirstController)SSDirector.GetInstance().CurrentSenceController;
this.leftLandModel = mainController.leftLandController.GetLandModel();
this.rightLandModel = mainController.rightLandController.GetLandModel();
this.boatModel = mainController.boatController.GetBoatModel();
}
void Update()
{
if (!mainController.isRuning)
return;
if (mainController.time <= 0)
{
mainController.JudgeCallback(false, "Game Over!");
return;
}
this.gameObject.GetComponent<UserGUI>().gameMessage = "";
// 判断是否已经胜利
if (rightLandModel.priestNum == 3)
{
mainController.JudgeCallback(false, "You Win!");
return;
}
else
{
/*
判断是否已经失败
leftPriestNum: 左边牧师数量
leftDevilNum: 左边恶魔数量
rightPriestNum: 右边牧师数量
rightDevilNum: 右边恶魔数量
若任意一侧,牧师数量不为0且牧师数量少于恶魔数量,则游戏失败
*/
int leftPriestNum, leftDevilNum, rightPriestNum, rightDevilNum;
leftPriestNum = leftLandModel.priestNum + (boatModel.isRight ? 0 : boatModel.priestNum);
leftDevilNum = leftLandModel.devilNum + (boatModel.isRight ? 0 : boatModel.devilNum);
if (leftPriestNum != 0 && leftPriestNum < leftDevilNum)
{
mainController.JudgeCallback(false, "Game Over!");
return;
}
rightPriestNum = rightLandModel.priestNum + (boatModel.isRight ? boatModel.priestNum : 0);
rightDevilNum = rightLandModel.devilNum + (boatModel.isRight ? boatModel.devilNum : 0);
if (rightPriestNum != 0 && rightPriestNum < rightDevilNum)
{
mainController.JudgeCallback(false, "Game Over!");
return;
}
}
}
}
FirstController.cs
JudgeCallback()函数来处理裁判类的反馈信息。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FirstController : MonoBehaviour, ISceneController, IUserAction
{
public CCActionManager actionManager;
public LandModelController rightLandController; // 右岸控制器
public LandModelController leftLandController; // 左岸控制器
public RiverModel riverModel; // 河流Model
public BoatModelController boatController; // 船控制器
public RoleModelController[] roleControllers; // 人物控制器集合
public bool isRuning; // 游戏进行状态
public float time; // 游戏进行时间
public void JudgeCallback(bool isRuning, string message)
{
this.gameObject.GetComponent<UserGUI>().gameMessage = message;
this.gameObject.GetComponent<UserGUI>().time = (int)time;
this.isRuning = isRuning;
}
// 导入资源
public void LoadResources()
{
// 人物初始化
roleControllers = new RoleModelController[6];
for (int i = 0; i < 6; i++)
{
roleControllers[i] = new RoleModelController();
roleControllers[i].CreateRole(PositionModel.roles[i], i < 3 ? true : false, i);
}
// 左右岸初始化
leftLandController = new LandModelController();
leftLandController.CreateLand("left_land", PositionModel.left_land);
rightLandController = new LandModelController();
rightLandController.CreateLand("right_land", PositionModel.right_land);
// 将人物添加并定位至左岸
foreach (RoleModelController roleModelController in roleControllers)
{
roleModelController.GetRoleModel().role.transform.localPosition = leftLandController.AddRole(roleModelController.GetRoleModel());
}
// 河流Model实例化
riverModel = new RiverModel(PositionModel.river);
// 船初始化
boatController = new BoatModelController();
boatController.CreateBoat(PositionModel.left_boat);
// 数据初始化
isRuning = true;
time = 60;
}
// 移动船
public void MoveBoat()
{
// 判断当前游戏是否在进行,同时是否有对象正在移动
if ((!isRuning) || actionManager.IsMoving())
return;
// 判断船在左侧还是右侧
Vector3 destination = boatController.GetBoatModel().isRight ? PositionModel.left_boat : PositionModel.right_boat;
actionManager.MoveBoat(boatController.GetBoatModel().boat, destination, 5);
// 移动后,将船的位置取反
boatController.GetBoatModel().isRight = !boatController.GetBoatModel().isRight;
}
// 移动人物
public void MoveRole(RoleModel roleModel)
{
// 判断当前游戏是否在进行,同时是否有对象正在移动
if ((!isRuning) || actionManager.IsMoving())
return;
Vector3 destination, mid_destination;
if (roleModel.isInBoat)
{
// 若人在船上,则将其移向岸上
if (boatController.GetBoatModel().isRight)
destination = rightLandController.AddRole(roleModel);
else
destination = leftLandController.AddRole(roleModel);
if (roleModel.role.transform.localPosition.y > destination.y)
mid_destination = new Vector3(destination.x, roleModel.role.transform.localPosition.y, destination.z);
else
mid_destination = new Vector3(roleModel.role.transform.localPosition.x, destination.y, destination.z);
actionManager.MoveRole(roleModel.role, mid_destination, destination, 5);
roleModel.isRight = boatController.GetBoatModel().isRight;
boatController.RemoveRole(roleModel);
}
else
{
// 若人在岸上,则将其移向船
if (boatController.GetBoatModel().isRight == roleModel.isRight)
{
if (roleModel.isRight)
{
rightLandController.RemoveRole(roleModel);
}
else
{
leftLandController.RemoveRole(roleModel);
}
destination = boatController.AddRole(roleModel);
if (roleModel.role.transform.localPosition.y > destination.y)
mid_destination = new Vector3(destination.x, roleModel.role.transform.localPosition.y, destination.z);
else
mid_destination = new Vector3(roleModel.role.transform.localPosition.x, destination.y, destination.z);
actionManager.MoveRole(roleModel.role, mid_destination, destination, 5);
}
}
}
// 游戏重置
public void Restart()
{
// 对各数据进行初始化
time = 60;
leftLandController.CreateLand("left_land", PositionModel.left_land);
rightLandController.CreateLand("right_land", PositionModel.right_land);
for (int i = 0; i < 6; i++)
{
roleControllers[i].CreateRole(PositionModel.roles[i], i < 3 ? true : false, i);
roleControllers[i].GetRoleModel().role.transform.localPosition = leftLandController.AddRole(roleControllers[i].GetRoleModel());
}
boatController.CreateBoat(PositionModel.left_boat);
isRuning = true;
}
void Awake()
{
SSDirector.GetInstance().CurrentSenceController = this;
LoadResources();
this.gameObject.AddComponent<UserGUI>();
this.gameObject.AddComponent<CCActionManager>();
this.gameObject.AddComponent<JudgeController>();
}
void Update()
{
if (isRuning)
{
time -= Time.deltaTime;
this.gameObject.GetComponent<UserGUI>().time = (int)time;
}
}
}
项目源代码与视频链接
运行效果:
成功情况:
失败情况:
3、材料与渲染联系【可选】
Albedo Color and Transparency
- Standard Shader 自然场景渲染器。
- 阅读官方 Standard Shader 手册 。
- 选择合适内容,如 Albedo Color and Transparency,寻找合适素材,用博客展示相关效果的呈现
我们常用Material来设置我们的游戏对象外观。但是,外观除了颜色RGB之外,还有一个透明度的选项,即Albedo Color and Transparency。
创建五个球和五个材料,对于材料的color设置上,统一为RGB=0,0,0,变动的是A属性。
注意:需要将材料的设置里面的Rendering Mode改为Transparent。
将材料赋给每个球,查看效果:
Reverb Zones
- 声音
- 阅读官方 Audio 手册
- 用博客给出游戏中利用 Reverb Zones 呈现车辆穿过隧道的声效的案例
在一个3d对象上加入 Audio Reverb Zone 和 Audio Source组件,如下:
接着,只需要去Asset Store上搜索需要的汽车声效,然后将该声音加入到Audio Source中即可。