空间与运动

空间与运动

这是3D游戏编程的第三次作业,花了很长时间,看博客内容你懂的(菜鸡本菜)。

说明文档

闪光点

  • 1 为太阳系中的星体增加了TrailRender的轨迹效果!
  • 2 太阳系使用天空盒背景以及太阳使用粒子效果、效果震感!
  • 3 以下三点都在牧师与魔鬼实验中的困难解决小节中:
  • 3.1 理清父子物体中position的关系,配以图示。
  • 3.2 正确地最小变化地修改字体大小(之所以说正确地,是因为很多博客都没正确处理字体大小该如何保持除了字体之外的属性完全不变化)。
  • 3.3 怎样最快调整相机(/风区/光线)的位置使得我们看得舒适、操作得畅快?



作业内容

1. 简答并用程序验证【建议做】
  • [Q] 游戏对象运动的本质是什么?
    • 游戏运动本质就是使用矩阵变换(平移、旋转、缩放)改变游戏对象的空间属性。具体而言,空间属性由Transforn来显现,而随着游戏循环的进行,每一帧都可以做出相应的属性调整,也就是属性对时间的函数。下面的代码就是利用每一帧的更新来更新游戏对象的运动。
      public class MoveLeft : MonoBehaviour {
          void Update() { // 每一帧进行调用
              this.transform.position += Vector3.left * Time.deltaTime;
          }
      }
      

  • [Q] 请用三种方法以上方法,实现物体的抛物线运动。(如,修改Transform属性,使用向量Vector3的方法…)
    • 运行提示:下面的代码的游戏对象是由脚本parabola.cs动态生成的,只需要将该脚本挂载到Main Camera或者空对象中即可点击运行,看到效果。

    • 方法1:修改Transform属性,使用

      using System.Collections;
      using System.Collections.Generic;
      using UnityEngine;
      
      public class parabola : MonoBehaviour {
      
        // Start is called before the first frame update
        public GameObject Sphere;
        // 重力加速度为10(单位)/s^2
        private float gravity = 10.0f;
        // 水平速度5(单位)/s
        private float speed_x = 5.0f;
        // 竖直方向初速度为0
        private float speed_y = 0f;
      
        void Start() {
          // 初始化球体
          Sphere = GameObject.CreatePrimitive(PrimitiveType.Sphere);
          Sphere.transform.position = new Vector3(0, 15, 0);
        }
      
        // Update is called once per frame
        void Update() {
          if(Sphere.transform.position.y <= 0) {
            return;
          }
          // vy = v0 + gt
          speed_y += gravity * Time.deltaTime;
          // 更新水平匀速运动
          Sphere.transform.position += Vector3.left * speed_x * Time.deltaTime;
          // 更新竖直方向的匀加速运动
          Sphere.transform.position += Vector3.down * speed_y * Time.deltaTime;
        }
      }
      
    • 方法2:使用向量Vector3做出抛物线运动的简便表达

      public class parabola : MonoBehaviour {
        // 为方便查看区别,除update方法之外与方法1保持一致
        // Update is called once per frame
        void Update() {
          if(Sphere.transform.position.y <= 0) {
            return;
          }
          // vy = v0 + gt
          speed_y += gravity * Time.deltaTime;
          // 创建抛物线运动的向量:
          // x为负是为了与上一次代码的Vector3.left对应
          Vector3 parabola = new Vector3(-speed_x * Time.deltaTime, -speed_y * Time.deltaTime, 0);
          // 更新位置
          Sphere.transform.position += parabola;
        }
      }
      
    • 方法3:使用transform.Translate方法更新位置

      public class parabola : MonoBehaviour {
        // 为方便查看区别,除update方法之外与方法1保持一致
        // Update is called once per frame
        void Update() {
          if(Sphere.transform.position.y <= 0) {
            return;
          }
          // vy = v0 + gt
          speed_y += gravity * Time.deltaTime;
          // 创建抛物线运动的向量:
          // x为负是为了与上一次代码的Vector3.left对应
          Vector3 parabola = new Vector3(-speed_x * Time.deltaTime, -speed_y * Time.deltaTime, 0);
          // 使用transform.Translate更新位置
          Sphere.transform.Translate(parabola);
        }
      }
      
    • 效果展示 (上面代码的运行效果都是一样的) :

      默认以Y=0的平面为地面,着地时停止运动,可查看前面的代码中做出的落地判断。


  • [Q] 写一个程序,实现一个完整的太阳系, 其他星球围绕太阳的转速必须不一样,且不在一个法平面上。

    • 1.0版本

      • 预制制作:

        • 首先在网站太阳系贴图下载太阳系各星体的贴图图片到Resources/Pictures下;
        • 得到贴图后,我们为每个星体制作预制。制作预制在之前的作业已经做过,很简单,我们首先创建一个3D球体,并将刚才得到的贴图直接拖拽到物体上,即得到对应星体的预制;我们将这些预制都拖到Resources/Prefabs目录下,用于后面代码动态载入。
      • 位置安排:

        • 创建SolarSystem.cs脚本并挂载在Main Camera上,我们将上面的预制动态加载到场景中,这同时需要加载预制、放置合适位置和设置合适大小,所以不妨先将其抽取出一个LoadPrefab函数:

          private Transform LoadPrefab(string path, int locate, int radius) {
            // 加载预制
            var res = Resources.Load<Transform>(path);
            // 实例化
            Transform sphere = Instantiate(res, new Vector3(locate, 0, 0), Quaternion.identity);
            // 调整星体大小
            radius *= 2; // 由于操作的是球体,其Scale设置为直径
            sphere.localScale = new Vector3(radius, radius, radius);
            // 返回
            return sphere;
          }
          
        • 利用刚才的函数,我们为每个星体设定合适的大小,并且在设置位置时遵循:除太阳外,每个星体与上一个星体之间的间隔是该星体的半径。之所以这样设置是因为更新中心点坐标的代码可以复用:

          /*  // 八大行星
          public Transform mercury, venus, earth, mars, jupiter, saturn, uranus, neptune;
          	 // 太阳和月亮
          public Transform sun, moon;
          */
          void Start() {
            // 加载预制
            // 中心点,半径
            int locate = 0, radius = 0;
          
            // 太阳
            locate = 0;
            radius = 20;
            sun = LoadPrefab("Prefabs/Sun", locate, radius);
          
            // 水星
            locate += radius;
            radius = 2; // 往后的代码只需要修改半径
            locate += 2 * radius;
            mercury = LoadPrefab("Prefabs/Mercury", locate, radius);
          
            // 金星
            locate += radius;
            radius = 4;
            locate += 2 * radius;
            venus = LoadPrefab("Prefabs/Venus", locate, radius);
          
            // 篇幅原因,其他星体的设置不放上来了
            // 可以到下面给出的gitee仓库里面查看
          }
          
        • 运行上述代码,我们将得到我们初始位置的排布,如下图所示:

      • 设置转动:

        • 利用RotateAround实现公转,Rotate实现自转;每个星体旋转的法平面不同(PS:月球与地球共面使得轨迹观察起来比较正常)。
          void Update() {
            // RotateAround公转
            // Rotate自转
            mercury.RotateAround(sun.position, new Vector3(0, 2, -1), 50 * Time.deltaTime);
            mercury.Rotate(Vector3.up * 50 * Time.deltaTime);
          
            venus.RotateAround(sun.position, new Vector3(0, 5, -3), 40 * Time.deltaTime);
            venus.Rotate(Vector3.up * 40 * Time.deltaTime);
          
            earth.RotateAround(sun.position, new Vector3(0, 2, 1), 30 * Time.deltaTime);
            earth.Rotate(Vector3.up * 30 * Time.deltaTime);
          
            mars.RotateAround(sun.position, new Vector3(0, 5, 1), 22 * Time.deltaTime);
            mars.Rotate(Vector3.up * 20 * Time.deltaTime);
          
            jupiter.RotateAround(sun.position, new Vector3(0, 4, 9), 20 * Time.deltaTime);
            jupiter.Rotate(Vector3.up * 16 * Time.deltaTime);
          
            saturn.RotateAround(sun.position, new Vector3(0, 2, 5), 18 * Time.deltaTime);
            saturn.Rotate(Vector3.up * 14 * Time.deltaTime);
          
            uranus.RotateAround(sun.position, new Vector3(0, 3, 3), 16 * Time.deltaTime);
            uranus.Rotate(Vector3.up * 12 * Time.deltaTime);
          
            neptune.RotateAround(sun.position, new Vector3(0, 3, 7), 14 * Time.deltaTime);
            neptune.Rotate(Vector3.up * 10 * Time.deltaTime);
          
            // 月球的速度必须快,否则轨迹难以控制
            moon.RotateAround(earth.position, new Vector3(0, 2, 1), 500 * Time.deltaTime);
          }
          
      • 添加轨迹

        • 如果只是运行上面的代码的话,星体的运动看起来比较乱,于是我们可以为每个星体都添加一个TrailRender来渲染各自的运动轨迹。所以不妨抽取出函数AddTrail
          private void AddTrail(Transform trans, float time) { // time表示轨迹保留时间
            // 动态添加
            trans.gameObject.AddComponent<TrailRenderer>(); 
            // 设置时间,使得我们看到完整的轨迹圆
            trans.GetComponent<TrailRenderer>().time = time;
          }
          
        • 随后我们就可以在Start为除了太阳之外的星体调用该函数:
          void Start() {
            // ... 上面的代码保持不变
            // 增加轨迹
            const int time = 30;
            AddTrail(mercury, time);
            AddTrail(venus, time);
            AddTrail(earth, time);
            AddTrail(moon, 0.5f); // 避免月球轨迹影响其他观看
            AddTrail(mars, time);
            AddTrail(jupiter, time);
            AddTrail(saturn, time);
            AddTrail(uranus, time);
            AddTrail(neptune, time);
          }
          
      • 效果展示

      • 代码仓库

    • 2.0版本:太阳自发光 (使用粒子系统以及点光源)

      • 参考自官方教程:Unity 5.x/2017 标准教程

      • 制作并使用天空盒StarSky

        • 首先创建材质,并命名为StarSky
        • 进入该材质的Inspector,并将shader更改为Skybox/6 sided
        • 随后将星星图拖拽到六个HDR框框中,并更改exposure0.5则制作完成:
        • 依次选择WindowRenderingLighting,跳出Lighting弹窗,切换至environment选项,并将其下的Skybox Material设置为刚刚制作好的天空盒StarSky
        • 此时就能看到星空背景了:
      • 运用粒子系统制作太阳日冕效果

        • 首先下载上述教程中提供的smoke.tif
        • 依次选择GameObjectEffectsParticle System创建一个粒子系统,将其重命名为SunSurface并将trandform修改为如下:
        • SunSurfaceshape模块中修改如下参数:
        • Start Size右侧单击三角形并下拉菜单,选择Random Between Two Constants,并填入1025
          在这里插入图片描述
        • Start Speed设置为0
          在这里插入图片描述
        • 创建新材质命名为SunSurface,将其shader改为Particles/Additive (Soft)并将刚才的smoke.tif拖拽到它的Particle Texture中:
        • SunSurface材质拖入到粒子系统SunSurface的检视视图中的Render模块里的Material中并修改Max Particle Size1
        • 启用Color over Lifetime模块,单击颜色块,在弹窗中设置颜色大致如下:
        • Start Lifetime右侧单击三角形并下拉菜单,选择Random Between Two Constants,并填入255
          在这里插入图片描述
        • Emission模块里将Rate over Time设置为500
          在这里插入图片描述
        • Start Rotation右侧单击三角形并下拉菜单,选择Random Between Two Constants,并填入-180180
          在这里插入图片描述
        • 启用Rotation over Lifetime模块,并
          Augular Velocity右侧单击三角形并下拉菜单,选择Random Between Two Constants,并填入-1515
          在这里插入图片描述
        • 到这里就能看到粒子系统的效果:
      • 增加新太阳预制

        • 为了更好的展示粒子效果,太阳预制就不再使用太阳系1.0中的贴图了,我们新创建一个材质命名为SunColor,并调整Albedo#000000Emission#FFB200
        • 创建一个3D球体Sun,并将上面制作好的SunColor材质拖给它,并修改Transform中的Scale(20, 20, 20),粒子系统的半径10故此处直径为20s
        • 此时我们就能够和之前的粒子系统一起看到日冕效果:
        • 我们将球体Sun拖动到粒子系统SunSurface下,成为父子,随后我们为粒子系统SunSurface增加Light组件,调整TypePointRange1000,并修改Color
        • SunSurface拖到Resources/Prefabs下,形成预制。
      • 修改部分代码

        • 需要修改代码的地方只有Start函数:
          void Start() {
            // 加载预制
            // 中心点,半径
            int locate = 0, radius = 0;
            // 修改开始
            // 加载带有粒子效果太阳
            sun = LoadPrefab("Prefabs/SunSurface", locate, 0.5f);
            locate = 0;
            // 为的是位置平衡
            radius = 20;
            // 修改结束
            
            // 水星
            locate += radius;
            radius = 2; // 往后的代码只需要修改半径
            locate += 2 * radius;
            mercury = LoadPrefab("Prefabs/Mercury", locate, radius);
            // ...
          }
          
      • 效果展示

        • 上面的修改已经可以运行代码了,可以关掉默认光源,因为太阳已经可以发光

        PS:动图经过压缩所以后面天空盒不清晰,建议使用我的项目代码运行观看。

      • 代码仓库

2. 编程实践
  • 阅读以下游戏脚本

    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

      • 3个牧师、3个魔鬼、2个陆地、1条河流、1条船
    • 用表格列出玩家动作表(规则表),注意,动作越少越好

      玩家动作条件效果
      点击船上的牧师或魔鬼船在陆地侧而不是在河流中所点击的角色移动到陆地上
      点击陆地上的牧师或魔鬼船在陆地侧并且船未满员所点击的角色移动到船上
      点击船船在陆地侧并且船上至少有一个角色船移动至对面陆地
    • 请将游戏中对象做成预制:

      • 贴图:
      • 材质:
      • 做成预制:
    • 在场景控制器 LoadResources 方法中加载并初始化 长方形、正方形、球 及其色彩代表游戏中的对象。

      • 场景控制器即后面将会提及到的FirstController(实现了ISceneController接口),这里想要说的应该是如何通过代码加载预制:
        // 以加载船为例,首先加载预制资源
        var res = Resources.Load<GameObject>("Prefabs/Boat");
        // 再实例化该资源成为游戏对象
        GameObject boat = Object.Instantiate(res);
        
      • 当然MVC结构中,不会直接在场景控制器中调用上述的显示代码,而是将任务交接给对应Model的控制器
    • 使用 C# 集合类型 有效组织对象

    • 整个游戏仅 主摄像机 和 一个 Empty 对象, 其他对象必须代码动态生成!!! 。 整个游戏不许出现 Find 游戏对象, SendMessage 这类突破程序结构的 通讯耦合 语句。 违背本条准则,不给分

      • 下图为项目的游戏对象结构,其中main为空对象,挂载FirstController的脚本来动态实例所有的游戏对象。
    • 请使用课件架构图编程,不接受非 MVC 结构程序

    • 注意细节,例如:船未靠岸,牧师与魔鬼上下船运动中,均不能接受用户事件!

  • 游戏成品(动图高糊)

    • 游戏结束的判断

      • 某陆地魔鬼多于牧师

      • 超时

    • 行进中

      • 无法在船行进时点击或者对岸上船
    • 游戏取胜的判断

      • 左陆地只要有三个牧师即可通关
  • 代码

    • MVC结构:

      ModelControllerView
    • 类图:
      在这里插入图片描述

    • 类图中的细节:

      • 对于每类游戏对象,我们可以为其建立模型,比如牧师或魔鬼的模型RoleModel,该类会封装一些状态量,比如是否在船上、在左侧还是右侧等,还有加载预制并且实例化的方法。
      • 除了一些在游戏中静态不变的类(比如PositionModel存储初始位置信息,RiverModel与玩家没有交互功能),其他的模型都会有相应的控制器,用来控制模型的数据变化,这些控制器往上被场景控制器FirstController控制。除此之外,角色与船是可以响应玩家的点击动作的,所以每个RoleModel/BoatMode实例会添加Click组件,用于响应用户的点击。
      • 最后在视图上,上一点提到的RoleModelControllerBoatModelController都会将角色的移动或者船的移动这种视图变化转交给IUserAction接口来处理,IUserAction的具体实现类是UserGUI
    • 代码仓库:由于代码太长,就不全部贴上来了,具体的设计思路就是根据上面的MVC框架来实现每个模型、控制器。想要查看代码细节可以到gitee仓库中查看。

  • 实验中遇到的小困难的解决

    • 子物体相对于父物体的位置:

      • 以我们游戏预制作为讨论对象,我们知道在船上和陆地上会将物体作为子物体的形式存在,这有便于移动船的时候将物体也进行移动。那么我们就要处理好世界坐标和相对坐标两个概念。

      • 如下图所示,是游戏在世界坐标系下的长度图示:

      • 如果我们将一个物体拖入到另一物体中,后者成为父物体,其的Scale将会起到重要作用,由于前者需要在后者的坐标系中确定positionscale(此时与世界坐标系无关系)并且还要保持我们在世界坐标系中看到正确的长度,所以我们必须要将子物体的Scale以父物体的Scale刻度来重新计算。

      • 如上图所示,世界坐标系中12单位的长度在本父物体的坐标系中只是1单位,所以陆地上角色的中心距离陆地的中心的相对距离在世界坐标为3.5单位,但在父物体也即陆地的坐标系中仅仅是(3.5/5 = 0.7)单位。

    • 正确地设置不同UI控件地字体大小:

      • 之所以要强调正确,是因为我找到很多博客写的调节空间字体大小的都如下所示不负责任
        GUIStyle style = new GUIStyle {fontSize = 25};
        GUI.Label(new Rect(0, 0, 100, 50), "SEEMS_OK", style);
        
        乍看之下,感觉没毛病,但同样的代码要是直接运用到Button你会发现按钮就只剩下字体,背景和边框都没了。正确的方式应该是尽量保证所改变属性之外的样式能够继续保持。
        int memo = GUI.skin.GetStyle("button").fontSize; // 记录按钮的默认字体大小
        GUI.skin.GetStyle("button").fontSize = 30; // 改为期望值
        GUI.Button(new Rect(0, 0, 50, 50), "RIGHT!");
        GUI.skin.GetStyle("button").fontSize = memo; // 恢复原来的值
        
        这样子我们就能够看到按钮的字体变大而其他样式保持不变。但是我们很快就会发现,我们GetStyle改的是系统的全局样式,也就是说,当你第二次运行游戏时,memo早就记忆着你最近的一次改变,同时我们不管任何Button都会使用这个skin。于是最后我想到应该考虑如何克隆一个skin副本,但其实这还不如仅拷贝GUIStyle,于是我们有了最后的解决方案:
        // 获取按钮默认样式的拷贝
        GUIStyle myBtn = new GUIStyle("button");
        myBtn.fontSize = 25;
        // 指定按钮为修改过后的按钮样式
        GUI.Button(new Rect(0, 0, 50, 50), "EASY!!", myBtn);
        // 下面的按钮仍旧使用的是默认的按钮样式
        GUI.Button(new Rect(50, 50, 50, 50), "DEFAULT");
        
        可以看到,上面的获取默认按钮样式的方式十分简单,竟然只要传strng即可??其实官方文档并没有直接传string这个API或许可以在后续的博客讲解下这是如何做到的,先卖个关子。

    • 简便的切换主相机视角的方式:

      • 作为新手,时常烦恼如何调整主相机到我们想要的视角,于是我们在相机移动和旋转之间来回磨蹭 ,这样的确是效率很低,除了主相机,还有很多关于方向的游戏对象也有一样的问题,比如风区(Wind Zone)或者平行光线。
      • 救星: 我们习惯使用手工具和Alt+拖拽来移动来将我们的Scene场景来到想要的角度,但是这是主相机并不停留在该视角前,但是我们只需要:
        • Hierarchy视图中选中主相机(或者风区、光线);
        • 按下快捷键Ctrl + Shift + F 或者 点击菜单栏中GameObject并点击Align With View
        • 这样就完成了主相机视角的切换了。

3. 思考题【选做】
  • [Q] 使用向量与变换,实现并扩展 Tranform 提供的方法,如 RotateRotateAround 等。
    • 实现Rotate
      private void Rotate(Transform trans, Vector3 eulers) {
        trans.rotation *= Quaternion.Euler(eulers);
      }
      // 另一个重载的版本
      private void Rotate(Transform trans, Vector3 axis, float angles) {
        trans.rotation *= Quaternion.AngleAxis(angles, axis);
      }
      
    • 实现RotateAround
      private void RotateAround(Transform trans, Vector3 point, Vector3 axis, float angles) {
        var rotate = Quaternion.AngleAxis(angles, axis);
        var diff = trans.position - point;
        diff = rotate * diff;
        trans.position = point + diff;
        trans.rotation *= rotate;
      }
      
    • 检测效果,首先我在三个位置分别放置了两个同样的立方体,我分别测试三个上面的函数,对于每一个测试,涉及两个物体,并且其中一个物体使用系统自带的Rotate/RotateAround,另一个则使用我实现的,如果我在游戏界面只看到三个活动的物体(即每组运动的物体重合了)则基本验证我的实现正确(下面提供的是主要代码):
      void Start() {
        cubes = new GameObject[6];
        for (int i = 0; i < 6; i++) {
          cubes[i] = GameObject.CreatePrimitive(PrimitiveType.Cube);
          if (i < 2) {
            cubes[i].transform.position = new Vector3(0, 0, 0);
          } else if (i < 4) {
            cubes[i].transform.position = new Vector3(5, 0, 0);
          } else {
            cubes[i].transform.position = new Vector3(10, 0, 0);
          }
        }
      }
      
      void Update() {
        // 第一组测试
        cubes[0].transform.Rotate(new Vector3(35, 45, 55) * Time.deltaTime);
        Rotate(cubes[1].transform, new Vector3(35, 45, 55) * Time.deltaTime);
      
        // 第二组测试
        cubes[2].transform.Rotate(new Vector3(1, 1, 5), 45 * Time.deltaTime);
        Rotate(cubes[3].transform, new Vector3(1, 1, 5), 45 * Time.deltaTime);
      
        // 第三组测试
        cubes[4].transform.RotateAround(new Vector3(1, 2, 5), new Vector3(1, 6, 9), 45 * Time.deltaTime);
        RotateAround(cubes[5].transform, new Vector3(1, 2, 5), new Vector3(1, 6, 9), 45 * Time.deltaTime);
      }
      
    • 运行效果:
      在这里插入图片描述
      与预期相符合。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CharlesKai

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值