3D游戏编程作业4

该博客介绍了在Unity3D中使用Fantasy Skybox构建3D游戏场景的步骤,包括导入资源、创建地形、设置天空盒和摄像机。此外,还详细阐述了游戏编程实践中的动作分离思想,目标是完成牧师与魔鬼游戏,并设计裁判类以实现游戏结束条件的判断。最后,博主提供了代码地址。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、基本操作演练【建议做】

1、下载Fantasy Skybox FREE,构建自己的游戏场景。

1.1 导入Fantasy Skybox FREE资源

在Windows->Asset Store中找到对应的Fantasy Skybox FREE资源,然后下载资源并点击Import导入即可。
导入后,我们可以在Project中的Assets目录下找到对应的文件夹,里面包含此天空盒所对应的材料和场景。
在这里插入图片描述

1.2 创建一块用于制作场景的地

选择菜单栏中的GameObject->3D Object->Terrain,建立一块地,然后可以用unity提供的地形设计工具来改变这块地的地形和贴图。
在这里插入图片描述
最上面一行是unity提供的简单工具,从左到右依次是:创建相邻地块、绘制地形、种树、种草和设置。
而在绘制地形中又有多种选项,分别是:
升高或降低地形、绘制洞、绘制纹理、设置高度、平滑高度、绘制有特定地质特征的纹理。

这里我没有使用自己绘制的地形(太丑了),将地形绘制工具的各种画笔功能尝试了几遍后,便用天空盒中附带的地形(种了一些树和花草完善原来的地形)来接着后面的操作了。

1.3 创建一个天空盒

选择菜单栏中的Assets->Create->Material,创建一个新的材料,命名为skybox。
然后选择Shader,在下拉栏中找到Skybox选项,将着色器改为天空盒模式,在Skybox中有四种模式,分别是:六面体天空盒、立体天空盒、全景天空盒、纯手写天空盒
在这里插入图片描述
这里我们用Cubemap,创建好天空盒材料后,将选定好的图片加进去,便成了我们所要的天空盒了。

1.4 将天空盒加入摄像机组件中

选择摄像机,在右边的Inspector中选择Add Component添加组件,在下拉栏中找到Rendering,里面便有我们需要的skybox组件,添加后,将我们所建立的天空盒加到Custom Skybox中,这样子天空盒便加入到摄像机组件中了。

1.5 运行截图

在这里插入图片描述

2、总结

在unity中,有很多种游戏对象,包括空对象、3D游戏对象、2D游戏对象、事件、灯光、声音、视频、UI界面、摄像机等。其中我们最常用的就是3D游戏对象、灯光、声音、UI界面和摄像机。
在这里插入图片描述
游戏对象是游戏中人物、道具、场景等对象的总称,它们有基础的属性,但没有具体的独特的功能,用于容纳不同的组件。只有挂载了组件后,一个游戏对象才有了属于它的功能。

二、编程实践

1、目标

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

2、回顾

在上一节课中,我们也是完成的牧师与魔鬼游戏,当时是利用基础的MVC架构,来设计牧师与魔鬼。下面回顾一下MVC架构:

Model

模型,负责建立起各个游戏对象和它们的基本行为,在牧师与魔鬼中的模型有牧师、魔鬼、船、河以及2个河岸。其中船和河岸被设计为容器,可以承载牧师和魔鬼这两个角色并记录它们的位置;牧师和魔鬼则是作为游戏的主要角色,可以在船和河岸之间来回移动。

View

视图,与用户交互的接口,是我们用户最终可以看到的界面。它能够接收用户的输入信息(比如点击或者键盘按键输入),并根据所点击的游戏对象不同而传递不同的信息,并把一些重要信息反馈给用户。

Controller

控制器,用于将模型和视图连接起来,以达到全局控制的目的。控制器不仅要从模型那里获取相应的信息,根据这些信息判断模型的位置和状态,而且还需要从视图处获得用户的输入信息,进行相应的物体操作。最后控制器还需要判断游戏的进行程度,判断游戏的进度和输赢状态,并将输赢信息返回到视图上,反馈给用户。

3、动作分离思路

从回顾中可以看出,控制器Controller所承担的工作太多了,在设计起来显得很复杂,很容易出现错误。所以动作分离的要求便应运而生。
动作分离,顾名思义,就是将动作从控制器中分离出来,这里需要分离出来的有船的移动和角色(牧师和魔鬼)的移动。

4、动作分离实现

这里的实现很大程度上参照了老师所给的课程文档中的4.2 核心代码与设计解读

  1. 首先,我们要定义一个动作的基类,用于存储动作的基本运动:平移、旋转和缩放。
    在这个基类中,我们可以没有区分是具体的哪一个动作,也就是说,如果我们直接调用基类,并不会有任何操作,所以这里实现了虚函数,以便后面子类进行重写。
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; }

    public virtual void Start () {
        throw new System.NotImplementedException ();
    }

    public virtual void Update () {
        throw new System.NotImplementedException ();
    }
}
  1. 接着,我们在动作基类的基础上,实现这一次作业所需要的具体动作。
    而在牧师与魔鬼游戏中,船只跟角色都只需要平移即可,不用旋转和缩放,所以我们的具体动作只需要实现平移操作,让物体从一个位置以一定的速度平移到目标位置。
public class SSMoveToAction : SSAction {
    public Vector3 target;
    public float speed;

    private SSMoveToAction () { }
    public static SSMoveToAction GetSSAction (Vector3 target, float speed) {
        SSMoveToAction action = ScriptableObject.CreateInstance<SSMoveToAction> ();
        action.target = target;
        action.speed = speed;
        return action;
    }

    public override void Start () {

    }

    public override void Update () {
        this.transform.position = Vector3.MoveTowards (this.transform.position, target, speed * Time.deltaTime);
        if (this.transform.position == target) {
            this.destroy = true;
            this.callBack.SSActionEvent (this);
        }
    }
}
  1. 在具体的简单动作基础上,又延伸出了组合动作,让物体通过不同动作的组合来到达终点。
    在游戏中,由于我们河岸的位置是比河要高的,也就是说,角色的水平位置比船高,那么如果我们直接采用一段平移过去的话,角色就会穿过河岸陷入地底然后到达船上,这明显是不合理的。所以我们需要将角色从河岸刀船上分解为两段平移:先平移到船的上方,再落下到船上。
public class CCSequenceAction : SSAction, ISSActionCallback {
    public List<SSAction> sequence;
    public int repeat = -1;
    public int start = 0;

    public static CCSequenceAction GetSSAcition (int repeat, int start, List<SSAction> sequence) {
        CCSequenceAction action = ScriptableObject.CreateInstance<CCSequenceAction> ();
        action.repeat = repeat;
        action.sequence = sequence;
        action.start = start;
        return action;
    }

    public override void Update () {
        if (sequence.Count == 0) return;
        if (start < sequence.Count) {
            sequence[start].Update ();
        }
    }

    public void SSActionEvent (SSAction action) {
        action.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);
            }
        }
    }

    public override void Start () {
        foreach (SSAction action in sequence) {
            action.gameObject = this.gameObject;
            action.transform = this.transform;
            action.callBack = this;
            action.Start ();
        }
    }

    void OnDestroy () {
        foreach (SSAction action in sequence) {
            Destroy (action);
        }
    }
}
  1. 设计好动作后,我们需要能够调用动作,也就是说,我们需要一个类,来管理这些平移操作,让我们所设计好的动作最终能实现为我们所需要的动作。
    在这里,我们实现的是一个基类,只是将这些动作构造成一个队列,等待添加河删除,同时对外提供RunAction接口,让外界可以添加动作。
public class SSActionManager : MonoBehaviour {
    private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction> ();
    private List<SSAction> waitingForAdd = new List<SSAction> ();
    private List<int> waitingForDelete = new List<int> ();

    protected void Update () {
        foreach (SSAction action in waitingForAdd) {
            actions[action.GetInstanceID ()] = action;
        }

        waitingForAdd.Clear ();

        foreach (KeyValuePair<int, SSAction> pair in actions) {
            SSAction action = pair.Value;
            if (action.destroy) {
                waitingForDelete.Add (action.GetInstanceID ());
            } else if (action.enable) {
                action.Update ();
            }
        }

        foreach (int key in waitingForDelete) {
            SSAction action = actions[key];
            actions.Remove (key);
            Destroy (action);
        }

        waitingForDelete.Clear ();
    }

    public void RunAction (GameObject gameObject, SSAction action, ISSActionCallback callback) {
        action.gameObject = gameObject;
        action.transform = gameObject.transform;
        action.callBack = callback;

        waitingForAdd.Add (action);
        action.Start ();
    }

}
  1. 在动作管理基类的基础上,我们要进一步设计一个子类,以便我们的控制器可以调用,实现船只的移动和角色的移动。
public class ScenceActionManager : SSActionManager, ISSActionCallback {
    SSMoveToAction boatAction;
    CCSequenceAction characterAction;
    Controller controller;
    Judger judger;
    private void Start () {
        controller = SSDirector.getInstance ().currentSceneController as Controller;
        controller.actionManager = this;
        judger = controller.judger;
    }

    public void moveBoat (GameObject boat, Vector3 pos, float speed) {
        judger.setForbid (true);
        boatAction = SSMoveToAction.GetSSAction (pos, speed);
        Debug.Log ("Ready to run!");
        this.RunAction (boat, boatAction, this);
    }
    public void moveCharacter (GameObject chr, Vector3 pos, float speed) {
        judger.setForbid (true);
        Vector3 start = chr.transform.position;
        Vector3 tmp = pos;
        if (start.y > pos.y) {
            tmp.y = start.y;
        } else if (start.y < pos.y) {
            tmp.x = start.x;
        }
        SSAction act1 = SSMoveToAction.GetSSAction (tmp, speed);
        SSAction act2 = SSMoveToAction.GetSSAction (pos, speed);
        characterAction = CCSequenceAction.GetSSAcition (1, 0, new List<SSAction> { act1, act2 });
        this.RunAction (chr, characterAction, this);
    }
    public void SSActionEvent (SSAction action) {
        judger.setForbid (false);
    }
}

5、裁判类的实现

2019年新加的要求需要我们设计一个裁判类,其实很简单,就是将原来放在控制器中的判断输赢的函数分离出来,单独成为一个类就好了。

public class Judger : MonoBehaviour {
    public int checkGame()
    {
        Controller controller = SSDirector.getInstance().currentSceneController as Controller;
        int leftP = 0, rightP = 0, leftD = 0, rightD = 0;
        int[] LCount = controller.leftBank.getCount();
        leftP += LCount[1];
        leftD += LCount[0];

        int[] RCount = controller.rightBank.getCount();
        rightD += RCount[0];
        rightP += RCount[1];

        int[] Bcount = controller.boat.getCount();
        //Debug.Log(Bcount[0] + " " + Bcount[1]);


        if (leftD + leftP == 6)		// win
			return 2;
        
        // 计算上船上的人
        if (controller.boat.getLR() == 0) {
            leftP += Bcount[1];
            leftD += Bcount[0];
        }
        else if (controller.boat.getLR() == 1) {
            rightP += Bcount[1];
            rightD += Bcount[0];
        }

        Debug.Log("RP: " + rightP + " RD: " + rightD); //测试用

        // 如果魔鬼数量大于牧师,并且牧师数量不为0就失败!
        if (leftP < leftD && leftP != 0) {
            return 0;
        }
        if (rightP < rightD && rightP != 0) {
            return 0;
        }
        return 1;
    } 

    public void setForbid(bool b) {
        Controller controller = SSDirector.getInstance().currentSceneController as Controller;
        controller.forbid = b;
    } 
}

6、挂载运行

创建一个空游戏对象,将设计好的Controller文件挂载上去即可。(注意:这里要先做好船只、河岸、河、牧师和魔鬼的预制)。
在这里插入图片描述

运行场景

截图

游戏录屏

录屏

三、代码地址

Gitee传送门

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值