3D游戏编程与设计——游戏对象与图形基础章节作业与练习

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})";
}
  1. 可以使用 readonly 修饰符来声明结构类型不可变
  2. readonly 结构的所有数据成员都必须是只读的
  3. 不能声明无参数构造函数。
  4. 不能在声明实例字段或属性时对它们进行初始化。
  5. 结构类型的构造函数必须初始化该类型的所有实例字段。
  6. 结构类型不能从其他类或结构类型继承,也不能作为类的基础类型。 但是,结构类型可以实现接口。
  7. 不能在结构类型中声明终结器。
  8. 可以在结构类型的声明中使用 ref 修饰符。 ref 结构类型的实例在堆栈上分配,并且不能转义到托管堆。
  • 枚举类型

枚举类型 是由基础整型数值类型的一组命名常量定义的值类型。

enum Season
{
    Spring,
    Summer,
    Autumn,
    Winter
}

默认情况下,枚举成员的关联常数值为类型 int;它们从零开始,并按定义文本顺序递增 1。 可以显式指定任何其他整数数值类型作为枚举类型的基础类型。

enum ErrorCode : ushort
{
    None = 0,
    Unknown = 1,
    ConnectionLost = 100,
    OutlierReading = 200
}
  1. 不能在枚举类型的定义内定义方法。若要向枚举类型添加功能,请创建扩展方法。
  2. 对于任何枚举类型,枚举类型与其基础整型类型之间存在显式转换。
  • 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的各项工具绘制地图, 包括造山,造草,添加细节等等。

将下载来的素材中的树和草等细节添加到图层中:

在这里插入图片描述

在这里插入图片描述

整体效果:

在这里插入图片描述

在这里插入图片描述

  • 写一个简单的总结,总结游戏对象的使用
  1. Camera:是游戏的眼镜,通过Camera来观察游戏世界。
  2. Light:光源,用来照明或者添加阴影。
  3. Empty空对象:作为载体,挂载游戏脚本或成为其他对象的父对象等用途。
  4. Cube等3D object:搭建游戏世界的组成元素,通过设置其Transform等属性来变换它们的位置、形态等。
  5. Terrain等:组成元素,又是编辑工具,例如Terrain本身是地图,然后又附带了绘制地图的各项工具(造山、造草等)。

2、编程实践

  • 牧师与魔鬼 动作分离版
    • 【2019开始的新要求】:设计一个裁判类,当游戏达到结束条件时,通知场景控制器游戏结束

项目要求:

  1. 使用专用的对象来管理运动。在本游戏中的运动即对象的移动,现在对象的移动由动作管理器接手,而不是上一个版本的MoveController,现在不再需要为每个对象都加上Move.cs。
  2. 设计一个裁判类,当游戏达到结束条件时,通知场景控制器游戏结束。游戏胜利与失败的条件均由JudgeController接管,FirstController新增处理JudgeController反馈的JudgeCallback函数,而取消了Check函数。

优点:代码结构更健全,方便添加新动作。

项目结构:

  1. 在上版本的MVC结构下,添加Actions,上版本中的MoveController和Move可以去除,由CCActionManager等来管理相关动作。
  2. 游戏中所有的移动(动作)都与FirstController分离开来,FirstController通过调用CCActionManager的函数来触发动作。
  3. 判断游戏状态的功能从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;
        }
    }
}

项目源代码与视频链接

gitee源码链接

项目运行效果mp4

运行效果:

成功情况:

在这里插入图片描述

失败情况:

在这里插入图片描述

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中即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值