空间与运动学

    科技2022-07-17  121

    空间与运动

    PS:所有代码请点击下方代码传送门

    代码传送门

    一、 简答并用程序验证

    1. 游戏对象运动的本质是什么?

      游戏对象运动的本质是每一帧中游戏对象在空间中 位置和角度 的变化。

    2. 请用三种方法以上方法,实现物体的抛物线运动。(如,修改Transform属性,使用向量Vector3的方法…)

    Method 1

      方法一利用公式: x = v x t , y = 1 2 g t 2 x = v_xt, y = \frac{1}{2}gt^2 x=vxt,y=21gt2 来直接计算出某一个时间点物体的位置。

    using System.Collections; using System.Collections.Generic; using UnityEngine; public class parabolic : MonoBehaviour { public float xSpeed = 10.0f; private float ySpeed = 0.0f; private float g = 9.8f; private float initX = 0.0f; private float initY = 10.0f; private float currentTime = 0.0f; // Update is called once per frame void Update() { // first method currentTime += Time.deltaTime; this.transform.position = new Vector3(initX+currentTime*xSpeed,initY-(ySpeed*currentTime+0.5f*g*currentTime*currentTime),0.0f); } }

    Method 2

      方法二采用累加计算的方式,以微分思想,每次计算: x + = v x ∇ t , y + = v y ∇ t , v x = v x , v y = v y + g ∇ t x += v_x\nabla t, y += v_y\nabla t, v_x = v_x, v_y = v_y+g\nabla t x+=vxt,y+=vyt,vx=vx,vy=vy+gt 从而计算出 t 时刻物体的位置。

    using System.Collections; using System.Collections.Generic; using UnityEngine; public class parabolic : MonoBehaviour { public float xSpeed = 10.0f; private float ySpeed = 0.0f; private float g = 9.8f; private float initX = 0.0f; private float initY = 10.0f; private float currentTime = 0.0f; // Start is called before the first frame update void Start() { // second method this.transform.position = new Vector3(initX,initY,0.0f); } // Update is called once per frame void Update() { // second method this.transform.position += new Vector3(xSpeed*Time.deltaTime,-ySpeed*Time.deltaTime,0.0f); ySpeed += g*Time.deltaTime; } }

    Method 3

      方法三不再通过直接修改 transform.position 来得到物体 t 时刻的位置,而是利用了封装好的函数:transform.Translate() 来得到 t 时刻物体的 position。

    using System.Collections; using System.Collections.Generic; using UnityEngine; public class parabolic : MonoBehaviour { public float xSpeed = 10.0f; private float ySpeed = 0.0f; private float g = 9.8f; private float initX = 0.0f; private float initY = 10.0f; private float currentTime = 0.0f; // Start is called before the first frame update void Start() { // third method this.transform.position = new Vector3(initX,initY,0.0f); } // Update is called once per frame void Update() { // third method this.transform.Translate(new Vector3(xSpeed*Time.deltaTime,-ySpeed*Time.deltaTime,0.0f)); ySpeed += g*Time.deltaTime; } }

    运行结果

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

      该程序的实现主要有以下几个步骤:

    制作出恒星 太阳、八大行星 以及围绕地球转的 月球编写围绕某一中心体旋转的脚本编写自转脚本将脚本挂载在合适对象上

    游戏对象制作

      制作过程也比较简单,单纯的工作量问题。步骤大致如下:

    创建球形对象搜索八大行星大小与位置关系依据上述关系调整球形对象大小与位置搜索对应行星贴图,并加载在球形对象上保存为预设

    围绕中心体旋转

      该代码其实较为容易编写,因为已经有现成的 transform.RotateAround() 函数可以使用,只需要随机获得旋转轴以及旋转速度即可。代码如下:

    using System.Collections; using System.Collections.Generic; using UnityEngine; public class rotateAroundOrigin : MonoBehaviour { public Transform Origin; private int speed; private int angleX; private int angleY; // Start is called before the first frame update void Start() { speed = Random.Range(30,70); angleX = Random.Range(0, 70); angleY = Random.Range(0, 70); } // Update is called once per frame void Update() { this.transform.RotateAround(Origin.position,new Vector3(angleX,angleY,0),speed*Time.deltaTime); } }

    自转

      自转则更为简单,只需随机获得自转速度,之后利用现成的 transform.Rotate() 函数即可实现自转。代码如下:

    using System.Collections; using System.Collections.Generic; using UnityEngine; public class selfRotate : MonoBehaviour { private int speed = 20; // Start is called before the first frame update void Start() { speed = Random.Range(10,50); } // Update is called once per frame void Update() { this.transform.Rotate(0,speed*Time.deltaTime,0,Space.Self); } }

    脚本挂载

      八大行星 以及 月球 都需要挂载 围绕旋转 以及 自转 的脚本,但是在围绕旋转的中心体上,八大行星是太阳,而月球是地球。

    额外制作

      如果单纯没有背景的话,整个运行说实话,挺丑的。于是,我就想着在整个太阳系背后放一个很大但很薄的物体(大:x, y 很大;薄:z 很小),然后再在物体上挂一张宇宙的图片,然后就完美了。   然而,现实给我来了一记小拳拳锤胸口,网上找到的合适的图分辨率都不够呀!!!!最终,挂载在背景上后,好像还是很丑,哭了(;´д`)ゞ

    最终效果

    <

    二、编程实践

    实验要求

    程序需要满足的要求:

    play the game ( http://www.flash-game.net/game/2535/priests-and-devils.html )列出游戏中提及的事物(Objects)用表格列出玩家动作表(规则表),注意,动作越少越好请将游戏中对象做成预制在场景控制器 LoadResources 方法中加载并初始化 长方形、正方形、球 及其色彩代表游戏中的对象。使用 C# 集合类型 有效组织对象整个游戏仅 主摄像机 和 一个 Empty 对象, 其他对象必须代码动态生成!!! 整个游戏不许出现 Find 游戏对象, SendMessage 这类突破程序结构的 通讯耦合 语句。 违背本条准则,不给分请使用课件架构图编程,不接受非 MVC 结构程序注意细节,例如:船未靠岸,牧师与魔鬼上下船运动中,均不能接受用户事件!

    开篇说明

      首先,需要感谢前人的博客:

    Unity3d学习之路-牧师与魔鬼。传送门Unity3d学习之路-牧师与魔鬼V2(动作分离版)。传送门

      就是可惜我不怎么能看懂动作分离版的实现逻辑,就只能大概阐述一下我所认为的动作分离。首先,有一个最终管理者:动作管理器。它管理着所有动作的执行,能判断动作是应该在等待,还是放到就绪队列。之后是它所管理的东西:单一动作和组合动作,组合动作又有单一动作合成。   那么,为什么要有这个玩意儿呢? 从我没有使用这个设计实现了 Move 之后,我就意识到这有多重要了。因为,一个 update,会将整个函数一次性执行完。因此,你以为是应该顺序执行的动作,实际上并不是,而是直接执行了最后一个。就比如想要先水平后垂直地运动到目标点。依据我的实现,就需要设定两次目标点,但直接这样做的最终结果就是,目标点始终是第二次设定的点,然后物体就一条直线地走到了目标。这就很尴尬了,需要加很多一些奇奇怪怪的逻辑,来最终实现能先水平后垂直。   所以呀,得有这个动作管理器来去管理到底要执行哪个动作,这样就能很方便的实现复杂的组合动作了。别看我,我学艺不精,也没实现成··········

      本次游戏实现,参考了前人博客,因此实现思路是一样的,导致需要的类变量以及类函数基本相同,因此最终实现上会看上去非常相似。但细看的话,还是能看出我在一些比较复杂的部分上的实现与博客中的实现是不同的,逻辑上有所区别,比如我认为初始位置的设定以及将角色加入到初始岸是应该作为初始化的一部分的,因此 reset() 时也应该这么做,但博客里跟我的想法不一样;再比如组合动作的实现等。   接下来,开始正题喽!

    游戏中提及的事物

    牧师恶魔河岸河流小船交互按键

    动作表

    玩家行为有效条件游戏执行点击岸上角色船与角色在同一边 && 船上有空位角色移动到船上点击船上角色船不在移动角色移动到岸上点击船船上有角色 && 船不在移动船移动到对岸

    游戏对象做成预制

    Boat:小船Devil:恶魔Priest:牧师River:河流Shore:河岸

    脚本逻辑顺序

      说实话,我觉得很多小伙伴在刚接触这MVC模式的时候应该都跟我一样非常懵逼吧。。。(不会就我一个懵吧)所以我认为,说明一下脚本逻辑是有必要的。我认为脚本可以按以下顺序来阅读,会比较轻松:

    Models.csFirstController.csUserGUI.cs

      首先是 Models.cs。该文件主要是实现游戏对象类,包括某类游戏对象的创建,以及该类对象所涉及的一些功能实现,比如此次的 Boat,要能移动,能知道船上有多少个人,是否还有空位等等。除此之外,也要将 最最关键 的接口和单实例类(导演)在这声明:

    ISceneControllerIUserActionSSDirector

      之后是 FirstController.cs。这个脚本我认为需要做的就是利用 Models.cs 中已经实现好的游戏对象类来实现上述的接口:ISceneController、IUserAction 中的函数。   最后是 UserGUI.cs。此脚本利用 OnGUI() 来实现用户界面以及之前所说的玩家行为交互。

    脚本编写

    Models.cs

    ISceneController

    public interface ISceneController{ void LoadResources(); }

    IUserAction

    public interface IUserAction{ // move boat void MoveBoat(); // move role void MoveRole(RoleModel role); void Restart(); // check whether game over or not int Check(); }

    SSDirector

    public class SSDirector : System.Object{ private static SSDirector _instance; public ISceneController CurrentScenceController { get; set; } public bool running{ get; set;} public static SSDirector GetInstance(){ if (_instance == null){ _instance = new SSDirector(); } return _instance; } public int getFPS(){ return Application.targetFrameRate; } public void setFPS(int fps){ Application.targetFrameRate = fps; } }

    ShoreModel

    成员变量

    shore:创建河岸对象shore_side:记录是哪边河岸role_positions[]:记录河岸能放角色的位置roles[]:存储角色 GameObject shore; int shore_side; // 0: left(end) side; 1: right(start) side; Vector3[] role_positions; RoleModel[] roles = new RoleModel[6];

    成员函数   对于河岸而言,我们需要有以下功能:初始化、获知某一河岸对象是哪一边河岸、获得空位位置、获得岸上角色数量、将角色加进岸上、移走岸上角色以及重置。

    ShoreModel:构造函数,初始化shoreGetShoreSideGetEmptyIndex:获得第几个位置是空位GetEmptyPosition:获得空位的具体坐标位置GetNumOfTwoRoles:分别获得岸上两种角色:牧师与魔鬼的数量AddRole:将角色加进 roles[] 中DeleteRoleByName:根据名字删除某个角色Reset:重置

    BoatModel

    成员变量

    boat:创建船对象boat_side:记录boat在哪一边的河岸boat_count:记录船上角色数量start_empty_pos:在开始岸(右岸)的空位位置end_empty_pos:在终点岸(左岸)的空位位置move:用于实现移动click:用于实现点击事件roles[]:保存船上角色 GameObject boat; int boat_side = 1; // 0: left(end) side; 1: right(start) side; int boat_count = 0; // number of roles on boat Vector3[] start_empty_pos; Vector3[] end_empty_pos; Move move; OnClick click; RoleModel[] roles = new RoleModel[2];

    成员函数   对于船而言,我们需要有以下功能:初始化、获知某一河岸对象是哪一边河岸、获得空位位置、获得船上角色数量、将角色加进船中、移走船上角色以及重置。

    BoatModel:构造函数,初始化boatGetBoat:获得boat对象GetBoatSideEmpty:判断船是否为空GetEmptyIndex:获得第几个位置是空位GetEmptyPosition:获得空位的具体坐标位置GetNumOfTwoRoles:分别获得船上两种角色:牧师与魔鬼的数量MoveToAnotherSide:移动到另一边河岸AddRole:将角色加进 roles[] 中DeleteRoleByName:根据名字删除某个角色Reset:重置

    RoleModel

    成员变量

    role:创建角色对象type:角色类型,0:牧师,1:恶魔on_boat:判断是否在船上click:实现点击事件move:实现移动now_shore:当前角色所在河岸,可为空 GameObject role; int type; // 0: Priest; 1: Devil bool on_boat; OnClick click; Move move; ShoreModel now_shore = (SSDirector.GetInstance().CurrentScenceController as FirstController).start_shore;

    成员函数   对于角色而言,我们需要有以下功能:初始化、获取角色类型、获取角色所在河岸、获取角色名、设置角色名、设置位置、判断是否在船上、移动到目标位置、设置响应变量以表示在船上、设置响应变量以表示在岸上以及重置。

    RoleModel:构造函数,初始化roleGetType:获取角色类型GetShore:获取角色所在河岸GetName:获取角色名SetName:设置角色名SetPosition:设置位置OnBoat:判断是否在船上MoveToDes:移动到目标位置GetOnBoat:设置响应变量以表示在船上GetOnShore:设置响应变量以表示在岸上Reset:重置

    其他模型

    Move

    成员变量

    someone_is_moving:静态变量,保存是否有物体在移动,以确保在移动时点击其他物体时物体不会移动speed:移动速度des:目标位置is_moving:判断当前挂载的对象是否正在移动method_to_move:移动方式,0:先水平后垂直,1:先垂直后水平

    成员函数

    Update:更新对象位置GetSomeoneIsMovingSetSomeoneIsMovingMoveToDes:设置挂载的对象需要移动的目的地位置,利用Update实现移动
    OnClick

    成员变量

    action:IUserGUIstore_role:存储了当前脚本所挂载的角色对象,作为 MoveRole(role) 的参数model_type:所挂载的游戏对象类型,0:role,1:boatmove:利用 move.GetSomeoneIsMoving() 来获知当前是否有对象正在移动,从而达到禁用点击事件的目的

    成员函数

    Start:初始化 actionSetType:设置 model_typeSetRole:设置 store_roleOnMouseDown:实现点击事件

    FirstController.cs

    成员变量

    start_shore:保存开始(右)河岸的对象实例end_shore:保存结束(左)河岸的对象实例boat:保存船的对象实例roles:保存角色的对象实例user_gui:保存用户界面 UserGUI 的对象实例 public ShoreModel start_shore; //开始陆地 public ShoreModel end_shore; //结束陆地 public BoatModel boat; //船 public RoleModel[] roles; //角色 UserGUI user_gui;

    成员函数

      场景控制器需要实现以下功能:加载资源、移动船、移动角色、重新开始、检查游戏是否结束。

    LoadResources:加载资源MoveBoat:移动船MoveRole:移动角色Restart:重新开始游戏,重置变量Check:检查游戏是否结束

    UserGUI.cs

    成员变量

    state:保存当前游戏状态show_rule:显示规则 private IUserAction action; public int state = 0; bool show_rule = false;

    成员函数

    OnGUI:设计用户界面

    最终效果

    游戏失败示例

    游戏成功过关示例

    三、使用向量与变换,实现并扩展 Tranform 提供的方法,如 Rotate、RotateAround 等

    PS:由于空间感不是特别强,所以想不出要如何通过所需要的相对坐标计算出绝对坐标,因此我实现RotateAround时默认环绕体是中心体的子对象。太菜了ヽ(ー_ー)ノ

    Rotate

    using System.Collections; using System.Collections.Generic; using UnityEngine; public class RotateQuatanion : MonoBehaviour { public int speed = 20; public Vector3 e = Vector3.up; public float angle = 90; private float accumulate = 0; // Update is called once per frame void Update () { if(accumulate < angle){ Quaternion q = Quaternion.AngleAxis (speed * Time.deltaTime, e); this.transform.localPosition = q*this.transform.localPosition; accumulate += speed*Time.deltaTime; } // this.transform.position = new Vector3(this.transform.position.x*(1+0.1f*Time.deltaTime),this.transform.position.y,this.transform.position.z*(1+0.1f*Time.deltaTime)); } }

    RotateAround

    using System.Collections; using System.Collections.Generic; using UnityEngine; public class RotateQuatanion : MonoBehaviour { public Transform test; public int speed = 20; // Update is called once per frame void Update () { Quaternion q = Quaternion.AngleAxis (speed * Time.deltaTime, test.transform.up); this.transform.localPosition = q*this.transform.localPosition; // this.transform.position = new Vector3(this.transform.position.x*(1+0.1f*Time.deltaTime),this.transform.position.y,this.transform.position.z*(1+0.1f*Time.deltaTime)); } }
    Processed: 0.013, SQL: 8