3D游戏学习 空间与运动

1. 简答并用程序验证

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

游戏对象的运动的本质是该对象在每一帧中相关属性的变化,包括 transform 组件中的 position、rotation、scale 属性的变化。前者的变化造成了游戏对象绝对位置或相对位置的变化,而后者的变化造成了游戏对象所处角度的变化。

1.2 实现物体抛物线运动

抛物线运动可以分解为两个不同方向的分运动,其中一个方向为匀速运动,另一个方向为匀加速运动(即物体的加速度方向与初始速度方向不同)。根据这一点,我们有很多种方法可以实现物体的抛物线运动。

  1. 直接修改 transform 的 position 属性:令物体在 x 轴方向上匀速运动,在 y 轴方向上匀加速运动:

    public class Move1 : MonoBehaviour
    {
        public float speed_x = 5; // x 方向速度
        public float speed_z = 0; // x 方向速度
        public float acceleration = 10; // z 方向加速度
    
        // Start is called before the first frame update
        void Start()
        {
            
        }
    
        // Update is called once per frame
        void Update()
        {
            this.transform.position += Vector3.down * Time.deltaTime * speed_z;
            this.transform.position += Vector3.right * Time.deltaTime * speed_x;
            speed_z += acceleration * Time.deltaTime;
        }
    }
    
  2. 使用向量 Vector3

    public class Move2 : MonoBehaviour
    {
        public float speed_x = 5; // x 方向速度
        public float speed_z = 0; // x 方向速度
        public float acceleration = 10; // z 方向加速度
    
        // Start is called before the first frame update
        void Start()
        {
            
        }
    
        // Update is called once per frame
        void Update()
        {
            this.transform.position += new Vector3(Time.deltaTime * speed_x, Time.deltaTime * speed_z, 0);
            speed_z += acceleration * Time.deltaTime;
        }
    }
    
  3. 利用重力实现抛物线运动

    public class Move3 : MonoBehaviour
    {
        // Start is called before the first frame update
        void Start () {
             Rigidbody a = this.gameObject.AddComponent<Rigidbody>();
             a.useGravity = true;
             a.velocity = Vector3.right * 5;
        }
    
        // Update is called once per frame
        void Update()
        {
    
        }
    }
    

1.3 实现完整的太阳系

太阳系
太阳系

通过在太阳和八大行星上应用该脚本,可以实现太阳与八大行星的自转与八大行星围绕太阳的公转。实现过程如下:

  • 通过使用随机数,我们可以计算得到一个随机的旋转轴;
  • 根据万有引力,我们可以计算得到行星绕太阳公转的角速度(为了使旋转效果更加明显,这里对八大行星的角速度按相同比例进行了增加);
  • 根据旋转轴与角速度,我们可以使用 transform.RotateAround() 函数实现行星围绕太阳的公转的公转;
  • 使用transform.Rotate() 可以实现太阳和八大行星的自转;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SolarSystem: MonoBehaviour
{
    private Vector3 selfPosition;
    private Vector3 sunPosition;
    private Vector3 axis;
    private float speed;

    // Start is called before the first frame update
    void Start()
    {
        // 计算位置、距离等信息
        sunPosition = GameObject.Find("Sun").transform.position;
        selfPosition = this.transform.position;
        Vector3 localPosition = selfPosition - sunPosition;
        double r = localPosition.magnitude;
        
        if(r == 0) // 如果该天体为太阳
        {
            // 因为太阳没有公转,所以旋转轴无意义,角速度为0
            axis = new Vector3(1, 1, 1);
            speed = 0;
        }
        else
        {
            // 计算旋转轴
            for(int i = 0; i < 3; i++)
            {
                axis[i] = Random.Range(1, 100);
            }
            for (int i = 0; i < 3; i++)
            {
                int index1 = (i + 1) % 3;
                int index2 = (i + 2) % 3;
                axis[i] = (-localPosition[index1] * axis[index1] - localPosition[index2] * axis[index2]) / localPosition[i];
                break;
            }

            // 计算角速度
            speed = 400.0f * (float)System.Math.Sqrt(1.0f / (r * r * r));
            Debug.Log(axis);
        }
    }

    // Update is called once per frame
    void Update()
    {
        // 公转
        this.transform.RotateAround(sunPosition, axis, speed * Time.deltaTime);
        // 自转
        this.transform.Rotate(Vector3.up * 30.0f * Time.deltaTime);
    }
}

2. 编程实践

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.

游戏开始前
游戏结束

2.1 游戏对象

牧师,魔鬼,河岸,船,河

2.2 玩家动作表(规则表)

玩家动作执行条件执行结果
点击牧师/魔鬼牧师/魔鬼与船在同侧河岸牧师/魔鬼移动到船上
点击船船上至少有一个牧师/魔鬼船移动到对岸

2.3 项目组织结构

Assets
├── Materials
├── Resources
│   ├── Perfabs
└── Scripts
    ├── BaseCode.cs
    ├── FirstController.cs
    ├── ClickGUI.cs
    └── UserGUI.cs

2.4 MVC 结构程序

  • Model:场景中所有的 GameObject 就是 Model,例如牧师、魔鬼、船等,他们受到相应 Controller 的控制。
  • View:在此项目中为 UserGUI 和 ClickGUI,他们展示游戏结果,并提供用户交互渠道(在本项目中表现为点击物体与按钮)。
  • Controller
    • 控制游戏对象的 Controller:例如 MyCharacterController 可以控制牧师和魔鬼;CoastController 可以控制河岸;BoatController 可以控制船;
    • 场景控制器 FirstController:控制场景中的所有对象,包括他们的加载,通信,用户输入等;
    • 最高层的 SSDirector:采用单例模式实现,控制场景的创建、切换、销毁、游戏暂停、游戏退出等最高层功能。
SSDirector 类
public class SSDirector : System.Object {
    private static SSDirector _instance;
    public SceneController currentSceneController { get; set; }

    public static SSDirector getInstance() {
        if(_instance == null) {
            _instance = new SSDirector();
        }
        return _instance;
    }
}

SSDirector 是最高层次的控制器,虽然它控制着场景的行为,但是并不需要知道行为实现的细节,这些细节交由下一层的场景控制器负责实现,即不同的场景控制器对同一行为可能有不同的实现方式。

SSDirector 使用单例模式实现,确保在整个游戏过程中只能同时存在一个 SSDirector,因此即使在不同的 script 中也可以通过 SSDirector 获得相同的 currentSceneController,方便类与类之间的通信。

SceneController 接口
public interface SceneController {
    void loadResources();
}

SceneController 接口是 SSDirector 控制 FirstController(场景控制器)的渠道,因为 FirstController 实现了该接口,所以 SSDirector 可以通过 currentSceneController(FirstController 类)调用 SceneController 接口中的方法,来对当前场景进行操作。

UserAction 接口
public interface UserAction {
    void moveBoat();
    void clickChatacter(MyCharacterController characterCtrl);
    void restart();
}

采用门面模式实现的用户行为接口,该接口定义了用户可以进行的三种操作:移动船,移动牧师/魔鬼,以及重新开始游戏。在 ClickGUI 和 UserGUI 这两个类中,都保存了一个 UserGUI 的引用。当 ClickGUI 监测到用户点击某个游戏对象时,就会调用相对应的处理方法(移动牧师/魔鬼,或移动船);当 UserGUI 监测到用户点击 Restart 按钮时,则重新开始新的游戏。

MyCharacterController 类
    public class MyCharacterController {
        readonly GameObject character;
        readonly Moveable moveableScript;
        readonly ClickGUI clickGUI;
        readonly int characterType; // 0->priest, 1->devil

        bool _isOnBoat;
        CoastController coastController;

        public MyCharacterController(string characterName) {
            if(characterName == "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;
            }
            moveableScript = character.AddComponent(typeof(Moveable)) as Moveable;

            clickGUI = character.AddComponent(typeof(ClickGUI)) as ClickGUI;
            clickGUI.setController(this);
        }

        public void setName(string name) {
            character.name = name;
        }

        public void setPosition(Vector3 pos) {
            character.transform.position = pos;
        }

        public void moveToPosition(Vector3 destination) {
            moveableScript.setDestination(destination);
        }

        public int getType() {
            return characterType;
        }

        public string getName() {
            return character.name;
        }

        public void getOnBoat(BoatController boatCtrl) {
            coastController = null;
            character.transform.parent = boatCtrl.getGameobj().transform;
            _isOnBoat = true;
        }

        public void getOnCoast(CoastController coastCtrl) {
            coastController = coastCtrl;
            character.transform.parent = null;
            _isOnBoat = false;
        }

        public bool isOnBoat() {
            return _isOnBoat;
        }

        public CoastController getCoastController() {
            return coastController;
        }

        public void reset() {
            moveableScript.reset();
            coastController = (SSDirector.getInstance().currentSceneController as FirstController).fromCoast;
            getOnCoast(coastController);
            setPosition(coastController.getEmptyPosition());
            coastController.getOnCoast(this);
        }
    }

MyCharacterController 封装了一个 GameObject,表示牧师或恶魔。

该类的构造函数对该游戏对象进行实例化,因此当 new MyCharacterController() 时,场景中将出现一个新的游戏角色。此外,构造函数将 clickGUI 挂载到这个角色上,监测“鼠标点击角色”的事件。

除了构造函数之外,该类定义了一系列关于牧师/魔鬼游戏对象的函数,将其提供给场景控制器调用,包括设置/获取变量、移动、重置等。

BoatController 与 CoastController 类
public class CoastController {
    readonly GameObject coast;
    readonly Vector3 from_pos = new Vector3(9, 1, 0);
    readonly Vector3 to_pos = new Vector3(-9, 1, 0);
    readonly Vector3[] positons;
    readonly int to_or_from;

    MyCharacterController[] passengerPlaner;

    public CoastController(string _to_or_from) {
        positons = new Vector3[] { new Vector3(6.5F, 2.25F, 0), new Vector3(7.5F, 2.25F, 0), new Vector3(8.5F, 2.25F, 0),
                                  new Vector3(9.5F, 2.25F, 0), new Vector3(10.5F, 2.25F, 0), new Vector3(11.5F, 2.25F, 0) };
        passengerPlaner = new MyCharacterController[6];

        if(_to_or_from == "from") {
            coast = Object.Instantiate(Resources.Load("Perfabs/stone", typeof(GameObject)), from_pos, Quaternion.identity, null) as GameObject;
            coast.name = "from";
            to_or_from = 1;
        }
        else {
            coast = Object.Instantiate(Resources.Load("Perfabs/stone", typeof(GameObject)), to_pos, Quaternion.identity, null) as GameObject;
            coast.name = "to";
            to_or_from = -1;
        }
    }

    public int getEmptyIndex() {
        for(int i = 0; i < passengerPlaner.Length; i++) {
            if(passengerPlaner[i] == null) {
                return i;
            }
        }
        return -1;
    }

    public Vector3 getEmptyPosition() {
        Vector3 pos = positons[getEmptyIndex()];
        pos.x *= to_or_from;
        return pos;
    }

    public void getOnCoast(MyCharacterController characterCtrl) {
        int index = getEmptyIndex();
        passengerPlaner[index] = characterCtrl;
    }

    public MyCharacterController getOffCoast(string passenger_name) { // 0->priest 1->devil
        for(int i = 0; i < passengerPlaner.Length; i++) {
            if(passengerPlaner[i] != null && passengerPlaner[i].getName() == passenger_name) {
                MyCharacterController characterCtrl = passengerPlaner[i];
                passengerPlaner[i] = null;
                return characterCtrl;
            }
        }
        Debug.Log("Can't find passenger on coast: " + passenger_name);
        return null;
    }

    public int get_to_or_from() {
        return to_or_from;
    }

    public int[] getCharacterNum() {
        int[] count = { 0, 0 };
        for(int i = 0;i < passengerPlaner.Length; i++) {
            if(passengerPlaner[i] == null) {
                continue;
            }
            if(passengerPlaner[i].getType() == 0) { // 0->priest, 1->devil
                count[0]++;
            }
            else {
                count[1]++;
            }
        }
        return count;
    }

    public void reset() {
        passengerPlaner = new MyCharacterController[6];
    }
}


public class BoatController {
    readonly GameObject boat;
    readonly Moveable moveableScript;
    readonly Vector3 fromPosition = new Vector3(5, 1, 0);
    readonly Vector3 toPosition = new Vector3(-5, 1, 0);
    readonly Vector3[] from_positions;
    readonly Vector3[] to_positions;

    int to_or_from = 1; // to->-1, from->1
    MyCharacterController[] passenger = new MyCharacterController[2];

    public BoatController() {
        to_or_from = 1;

        from_positions = new Vector3[] { new Vector3(4.5F, 1.5F, 0), new Vector3(5.5F, 1.5F, 0) };
        to_positions = new Vector3[] { new Vector3(-5.5F, 1.5F, 0), new Vector3(-4.5F, 1.5F, 0) };

        boat = Object.Instantiate(Resources.Load("Perfabs/Boat", typeof(GameObject)), fromPosition, Quaternion.identity, null) as GameObject;
        boat.name = "boat";

        moveableScript = boat.AddComponent(typeof(Moveable)) as Moveable;
        boat.AddComponent(typeof(ClickGUI));
    }

    public void Move() {
        if(to_or_from == -1) {
            moveableScript.setDestination(fromPosition);
            to_or_from = 1;
        }
        else {
            moveableScript.setDestination(toPosition);
            to_or_from = -1;
        }
    }

    public int getEmptyIndex() {
        for(int i = 0; i < passenger.Length; i++) {
            if(passenger[i] == null) {
                return i;
            }
        }
        return -1;
    }

    public bool isEmpty() {
        for(int i = 0; i < passenger.Length; i++) {
            if(passenger[i] != null) {
                return false;
            }
        }
        return true;
    }

    public Vector3 getEmptyPosition() {
        Vector3 pos;
        int emptyIndex = getEmptyIndex();
        if(to_or_from == -1) {
            pos = to_positions[emptyIndex];
        }
        else {
            pos = from_positions[emptyIndex];
        }
        return pos;
    }

    public void getOnBoat(MyCharacterController characterCtrl) {
        int index = getEmptyIndex();
        passenger[index] = characterCtrl;
    }

    public MyCharacterController getOffBoat(string passenger_name) {
        for(int i = 0;i < passenger.Length; i++) {
            if(passenger[i] != null && passenger[i].getName() == passenger_name) {
                MyCharacterController characterCtrl = passenger[i];
                passenger[i] = null;
                return characterCtrl;
            }
        }
        Debug.Log("Can't find passenger in boat: " + passenger_name);
        return null;
    }

    public GameObject getGameobj() {
        return boat;
    }

    public int get_to_or_from() { // to->-1, from->1
        return to_or_from;
    }

    public int[] getCharacterNum() {
        int[] count = { 0, 0 };
        for(int i = 0; i < passenger.Length; i++) {
            if(passenger[i] == null) {
                continue;
            }
            else if(passenger[i].getType() == 0) { // 0->priest, 1->devil
                count[0]++;
            }
            else {
                count[1]++;
            }
        }
        return count;
    }

    public void reset() {
        moveableScript.reset();
        if(to_or_from == -1) {
            Move();
        }
        passenger = new MyCharacterController[2];
    }
}

BoatController 和 CoastController 类似 MyCharacterController,封装了船和河岸对象。与 MyCharacterController 不同的是,这两个类是一种“容器”,需要容纳其他游戏角色(牧师/魔鬼)在他们的空位中,因此需要实现 getEmptyPosition 等相关函数来获取该类的空位。

我们可以注意到这两个类有部分方法是同名的,之所以将一个动作分开实现,是为了降低不同类之间的耦合度,令每个类只实现与其相关的部分。例如 MyCharacterController 中的 getOnBoat() 只应该操作 MyCharacterController 中的成员,BoatController 中的 GetOnBoat() 只应该操作 BoatController 中的成员。我们在 FirstController 中想让游戏角色上船的时候,两个类的 getOnBoat() 都要调用。

Moveable 类
public class Moveable : MonoBehaviour {
    readonly float speed = 20;

    int status; // 0->not moving, 1->moving to mid, 2-> moving to dest
    Vector3 dest;
    Vector3 middle;

    void Update() {
        if(status == 1) {
            transform.position = Vector3.MoveTowards(transform.position, middle, speed * Time.deltaTime);
            if(transform.position == middle) {
                status = 2;
            }
        }
        else if(status == 2) {
            transform.position = Vector3.MoveTowards(transform.position, dest, speed * Time.deltaTime);
            if(transform.position == dest) {
                status = 0;
            }
        }
    }

    public void setDestination(Vector3 _dest) {
        dest = _dest;
        middle = _dest;

        if(_dest.y == transform.position.y) { // boat moves
            status = 2;
        }
        else if(_dest.y < transform.position.y) { // character from coast to boat
            middle.y = transform.position.y;
        }
        else {
            middle.x = transform.position.x;
        }
        status = 1;
    }

    public void reset() {
        status = 0;
    }
}

Moveable 类用于挂载在游戏对象上,当 Controller 调用 setDestination() 时可以实现游戏对象的移动。在移动实现上,将移动分成两个阶段进行,物体先移动到 middle 位置,再移动到 dest 位置,使物体进行折线运动,避免物体进入河岸之中(穿模)。

ClickGUI 类
public class ClickGUI : MonoBehaviour {
    UserAction action;
    MyCharacterController characterController;

    public void setController(MyCharacterController characterCtrl) {
        characterController = characterCtrl;
    }

    void Start() {
        action = SSDirector.getInstance().currentSceneController as UserAction;
    }

    void OnMouseDown() {
        if(gameObject.name == "boat") {
            action.moveBoat();
        }
        else {
            action.clickChatacter(characterController);
        }
    }
}

ClickGUI 类是用来监测用户点击,并调用 SceneController 进行响应的。我们可以看到 UserAction action 实际上是 FirstController 的对象,它实现了 UserAction 接口。ClickGUI 与 FirstController 打交道,就是通过 UserAction 接口的 API。ClickGUI 不知道这些 API 是怎么被实现的,但它知道 FirstController 类一定有这些方法。

链接: 太阳系项目.
链接: 牧师与魔鬼项目.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值