牧师与魔鬼 -- version2 动作分离

一、基本操作演练

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

  • 下载天空盒 Fantasy Skybox FREE,它包含了一些天空贴图在这里插入图片描述

    将下载的天空盒文件导入
    在这里插入图片描述

  • 创建 Material(Assets 上下文菜单 -> create -> Material) 起名 mysky,在 Inspector 视图中选择 Shader -> Skybox -> 6Sided,并且将刚刚下载的天空盒图片依次加入到右面六张图片的空缺处,结果如图:
    在这里插入图片描述

  • 运行,就能够看到刚刚做好的天空盒
    在这里插入图片描述

  • 加上地形以及花草树木
    在这里插入图片描述

2、写一个简单的总结,总结游戏对象的使用

Unity 游戏对象主要涉及三种类

  • GameObject: Unity 场景中所有实体的基类
  • Component: 能附加到游戏对象的部件的基类
  • Component 的各种子类。包括空间与变换部件 Transform、各种 渲染部件Reander ,脚本部件,MonoBehaviour 的子类等等。

游戏对象的使用

  • 所有游戏对象都有Active属性,Name属性,Tag属性等。每个 GameObject 还包含一个 transform 组件,我们可以通过这个组件来使游戏对象改变位置,旋转和缩放。
  • 通过改变物体的 material ,我们可以赋予它们不同的材质或者说外观。
  • 将 GameObject 做成预制是一个重要的游戏对象的使用方法,可以大大减少重复劳动的工作。将 游戏对象 拖动并放置在 Assets 面板之上,就生成了一个预制件
  • 对 GameObject 来说最常用的添加功能的方法是添加组件(脚本),这样通过组合的方式既拓展了对象的功*能,还将组件与对象耦合度大大降低。

之前使用过的游戏对象

  • 3D 物体:Cube、Sphere、Capsule、Sylinder、Plane、Quad,由三角形网格构建的物体:如Terrain。

  • Empty (不显示却是最常用对象之一):作为子对象的容器或创建一个新的对象空间。

  • Camera 摄像机:观察游戏世界的窗口。
    Projection属性包括正交视图与透视视图。Viewport Rect:属性是控制摄像机呈现结果对应到屏幕上的位置以及大小。屏幕坐标系:左下角是(0, 0),右上角是(1, 1)。Depth属性是当多个摄像机同时存在时,这个参数决定了呈现的先后顺序,值越大越靠后呈现。

  • light 光源
    灯光类型(type)包括平行光(类似太阳光),聚光灯(spot),点光源(point),区域光(area,仅烘培用)

  • audio 声音
    将声音素材拖入摄像机就可以成为背景音乐。可以设置是否重复,音量等属性

  • skyboxes 天空盒
    天空是一个球状贴图,通常用 6 面贴图表示。
    使用方法有两步,第一为在摄像机添加天空组件。Component 加入skybox组件,第二为直接拖入天空材料(Material)。


二、编程实践

1、牧师与魔鬼 动作分离版

**游戏视频github 地址戳这里

在这里插入图片描述


面向对象的游戏编程

如果感觉到场记(控制器)管的事太多,不仅要处理用户交互事件,还要游戏对象加载、游戏规则实现、运动实现等等,而显得非常臃肿。一个最直观的想法就是让更多的人(角色)来管理不同方面的工作。显然,这就是面向对象基于职责的思考,例如:用专用的对象来管理运动,专用的对象管理播放视频,专用的对象管理规则。就和踢足球一样,自己踢5人场,一个裁判就够了,如果是国际比赛,就需要主裁判、边裁判、电子裁判等角色通过消息协同来完成更为复杂的工作。


动作管理器的设计思想

为了用一组简单动作组合成复杂的动作,我们采用 cocos2d 的方案建立与 CCAtion 类似的类。设计思路如下:

  • 设计一个抽象类作为游戏动作的基类
  • 设计一个动作管理器类管理一组游戏动作的实现类
  • 通过回调,实现动作完成时的通知

动作管理器的设计类图

在这里插入图片描述

动作管理器的代码与设计解读
  • SSAction (动作基类)

    动作基类继承于ScriptableObject, 是不需要绑定 GameObject 对象的可编程基类。这些对象受 Unity 引擎场景管理。
    动作基类中申明了 StartUpdate 的虚函数,通过重写可以实现多态,这样继承者可以明确使用 Start 和 Update 编程游戏对象行为。
    为了在完成动作后告知管理者,同时避免与动作管理者直接依赖,动作基类中定义了接口 ISSACtionCallback 实现消息通知,避免与动作管理者直接依赖。

  public class SSAction : ScriptableObject {
    public bool enable = true;		//允许动作发生
    public bool destroy = false;	//销毁动作
    public GameObject gameobject { get; set; }  //动作对象
    public Transform transform { get; set; }	//动作对象的 transform
    public ISSActionCallback callback { get; set; }	//动作管理者//确保使用者不能够随意的创建动作
    protected SSAction() { }//子类需要对 Start 和 Update 进行重写
    public virtual void Start() {
        throw new System.NotImplementedException();
    }public virtual void Update() {
        throw new System.NotImplementedException();
    }
}

  • CCMoveToAction(简单动作)

    实现将一个物体移动的目标位置,并通知 callback 对象任务已完成的功能。
    一个易错点在于,需要加上对 enable 的判断来决定是否需要移动物体,不能够让函数始终处于触发状态。因为当人物在跟随船移动的时候是不符合处于目标点的状态的,这样运行后会发现人物处于一个固定点,没有跟着船移动。

public class CCMoveToAction : SSAction {
    public Vector3 target;	//移动目标位置
    public float speed;		//移动速度
   
    //函数 GetSSAction 返回一个设定好目标和移动速度的 action 对象
    public static CCMoveToAction GetSSAction(Vector3 target, float speed) {
    	CCMoveToAction action = ScriptableObject.CreateInstance<CCMoveToAction>();
    	action.target = target;
    	action.speed = speed;
	return action; 
    }//每次 Update 时物体进行少量的移动,为了平滑的移动到目标位置,需要由继承了Monobehavior类的对象来不断调用CCMoveToAction实例的Update函数
    public override void Update() {
        //一个易错点在于,需要加上对enable的判断来决定是否需要移动物体,不能够让函数始终处于触发状态。因为当人物在跟随船移动的时候是不符合处于目标点的状态的,这样运行后会发现人物没有跟着船移动。
        if (enable) {
            this.transform.position = Vector3.MoveTowards(this.transform.position, target, speed);
            if (this.transform.position == target) {
                this.destroy = true;
                this.enable = false;
                this.callback.SSActionEvent(this);
            }
        }
    }public override void Start() {
        Update();
    }
}

  • SequenceAction (顺序动作组合类)

    顺序动作是标准的组合设计模式,被组合的对象和组合对象属于同一种类型。通过组合模式,我们能实现几乎满足所有越位需要、非常复杂的动作管理。
    在设计中需要让动作组合继承抽象动作,能够被进一步组合;实现回调接受,能接收被组合动作的事件;
    创建一个动作顺序执行序列,-1 表示无限循环,start 开始动作。
    Start 执行动作前,为每个动作注入当前动作游戏对象,并将自己作为动作事件的接收者
    Update 方法执行执行当前动作
    SSActionEvent 收到当前动作执行完成,推下一个动作,如果完成一次循环,减次数。如完成,通知该动作的管理者
    OnDestory 如果自己被注销,应该释放自己管理的动作。

public class SequenceAction : SSAction, ISSActionCallback {
    public List<SSAction> sequence;    
    public int repeat = -1;    //循环次数、-1表示无限循环       
    public int start = 0;      //进行到哪个动作的标识
    public static SequenceAction GetSSAcition(int repeat, int start, List<SSAction> sequence) {
    SequenceAction action = ScriptableObject.CreateInstance<SequenceAction>();
    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 source, SSActionEventType events = SSActionEventType.Completed) {
        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); 
            }
        }
    }public override void Start() {
        foreach (SSAction action in sequence) {
            action.gameobject = this.gameobject;
            action.transform = this.transform;
            action.callback = this;                
            action.Start();
        }
    }void OnDestroy() {
        //todo
    }
}

  • ISSActionCallback(动作事件接口)
public enum SSActionEventType : int { Started, Completed }
public interface ISSActionCallback {
    void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Completed);
}

  • SSActionManager(动作管理基类)

    管理 SequenceAction 和 SSAction,可以给它们传递游戏对象,让游戏对象做动作或是一连串的动作,控制动作的切换。
    SSActionManager 继承了 ISSActionCallback 接口,通过这个接口,当动作做完或是连续的动作做完时候会告诉 SSActionManager,然后 SSActionManager 去决定如何执行下一个动作。

public class SSActionManager : MonoBehaviour, ISSActionCallback {   
    //RunAction 函数为 Action 设定游戏对象以及动作结束后的通知者
    public void RunAction(GameObject gameobject, SSAction action, ISSActionCallback callback) {
        action.gameobject = gameobject;
        action.transform = gameobject.transform;
        action.callback = callback;
        action.Start();
    }public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Completed) {}}

  • CCActionManager (动作管理)

    在具体的动作管理中依照游戏要求需要实现两个动作,移动船只以及移动人物。
    船只只包含平移的动作,所以使用简单的 CCMoveToAction 动作来执行;人物的动作由跳起、跃下,以及水平移动组成,所以需要使用顺序动作来控制。

public class CCActionManager : SSActionManager {
    private CCMoveToAction moveBoat;	//移动船只
    private SequenceAction moveCharacter;public ISceneController controller;	//移动人物public void Start() {
    controller = SSDirector.getInstance().currentSceneController;
    controller.actionManager = this;
}//动作管理器继承了 MonoBehaviour,而动作本身不具有在每个 tick更新状态的功能,所以管理器在每个自身 Update 被调用的时候要检查所管理的动作的状态,并调用相应的Update函数。
public void Update() {
    if (moveBoat) moveBoat.Update();
    if (moveCharacter) moveCharacter.Update();
}public void MoveBoat(GameObject boat, Vector3 target, float speed) {
    //获取以特定速度移动到 target 的动作对象
    moveBoat = CCMoveToAction.GetSSAction(target, speed);
    //将动作对象与船只对象绑定,让船只在动作的指挥下移动
    RunAction(boat, moveBoat, this);
}//使用顺序动作来控制人物的动作:跳起、跃下,以及水平移动
public void MoveCharacter(GameObject character, Vector3 target, float speed) {
    Vector3 forward, up, down;
    
    //设置跳起的水平距离,根据人物所处位置决定差值为正数或负数
    float jumpDistance = target.x < 0 ? -8 : 8;
   
    List<SSAction> sequence;
    
    //人物跃下下方的船只
    if (character.transform.position.y > target.y) {
        forward = target;
        forward.x += jumpDistance;
        forward.y = character.transform.position.y;
        down = target;
        CCMoveToAction moveForward = CCMoveToAction.GetSSAction(forward, speed);
        CCMoveToAction jump = CCMoveToAction.GetSSAction(down, speed);
        //顺序动作由前进、跃下组成
        sequence = new List<SSAction> { moveForward, jump };
    }
    //人物跳上上方的河岸
    else {
        forward = target;
        up = character.transform.position;
        up.x += jumpDistance;
        up.y = target.y;
        CCMoveToAction moveForward = CCMoveToAction.GetSSAction(forward, speed);
        CCMoveToAction jump = CCMoveToAction.GetSSAction(up, speed);
        //顺序动作由跳起、前进组成
        sequence = new List<SSAction> { jump, moveForward };
    }//设定动作的执行次数为 1
    moveCharacter = SequenceAction.GetSSAcition(1, 0, sequence);
    RunAction(character, moveCharacter, this);
}}

裁判类的设计实现
  • 通知 ISceneController 的接口设计
public enum GameStatus : int { win, lose, playing }
public interface ISSJudgeCallback {
    void SSJudgeEvent(ISceneController source, GameStatus status = GameStatus.playing);
}

  • 裁判类 Judge 的设计实现
public class Judge :MonoBehaviour, ISSJudgeCallback {
    CharacterModel[] roles = new CharacterModel[6];
    ISceneController callback;
    BoatModel boat;int countOfDevil_1, countOfDevil_2, countOfPriest_1, countOfPriest_2;public void setJudge(CharacterModel[] characters, BoatModel boat, ISceneController source) {
    for(int i = 0; i < 6; i++) {
        this.roles[i] = characters[i];	//裁判需要监视的对象
    }
    this.boat = boat;
    callback = source;
}public void SSJudgeEvent(ISceneController source, GameStatus status = GameStatus.playing) {
    source.status = status;	//告知 ISceneController 此时的游戏状态
}public void Update() {
    if (!boat.isSailing()) return;  //在船行驶的时候才对游戏状态进行判断
    if (callback.getGameStatus() == GameStatus.playing) {
        countOfDevil_1 = countOfDevil_2 = countOfPriest_1 = countOfPriest_2 = 0;
        //对两岸的游戏人物分别计数,查看是否符合游戏结束的条件
        for (int i = 0; i < 6; i++) {
            if (roles[i].getCoastName() == "leftCoast") {
                if (roles[i].getType() == "priest") countOfPriest_1++;
                else countOfDevil_1++;
            }
            else {
                if (roles[i].getType() == "priest") countOfPriest_2++;
                else countOfDevil_2++;
            }
        }
        if (countOfDevil_1 > countOfPriest_1 && countOfPriest_1 > 0 || countOfDevil_2 > countOfPriest_2 && countOfPriest_2 > 0) {
            SSJudgeEvent(callback, GameStatus.lose);
        }
        else if (countOfPriest_1 + countOfDevil_1 >= 6) {
            SSJudgeEvent(callback, GameStatus.win);
        }
    }
}}

ISceneController 的重新设计

由于本次我们实现了将动作控制从 ISceneController 的工作中分离,所以需要对原先 ISceneController 直接控制船/人物模型的动作部分进行修改。
首先需要在 ISceneController 中添加成员 public CCActionManager actionManager,这样就可以通过对actionManager 的操控间接控制动作发生,并且不需要了解太多细节。

  • moveBoat
public void moveBoat() {
        if (boat.isEmpty() || boat.isSailing()) return;    //通过 actionManager 间接控制动作发生
    actionManager.MoveBoat(boat.getBoat(), boat.getAndSetAnotherPort(), 0.50f);
    for (int i = 0; i < 6; i++) {
        if (characters[i].isOnBoat()) {
            characters[i].setCoastName(boat.getCoastName());
        }
    }
}
  • moveCharacter
public void moveCharacter(CharacterModel character) {
        if (boat.isSailing()) return;
        if (character.isOnBoat()) {
            //character.setCoastName(boat.getCoastName());
            character.leaveBoat();
            boat.getOff(character.getSeatIndex());        
            Vector3 newPos;
        int index;
        if (character.getCoastName() == "leftCoast") {
            index = leftCoast.getVacantIndex();
            newPos = character.getCoastPosition(index);
            newPos.x = -newPos.x;
        }
        else {
            index = rightCoast.getVacantIndex();
            newPos = character.getCoastPosition(index);
        }
        character.setPosIndexOnCoast(index);
        actionManager.MoveCharacter(character.getCharacter(), newPos, 0.50f);
    }
    else {
        if (boat.isFull() || boat.getCoastName() != character.getCoastName()) return;
        int index = character.getPosIndexOnCoast();       
        if (character.getCoastName() == "leftCoast") leftCoast.getOff(character.getPosIndexOnCoast());
        else rightCoast.getOff(character.getPosIndexOnCoast());
        int seat = boat.getVacantIndex();
        character.setSeat(seat);
​
        actionManager.MoveCharacter(character.getCharacter(),boat.getBoat().transform.position + 
            character.getBoatPosition(seat), 0.50f);
        character.board(boat.getBoat().transform);
    }
}

三、材料与渲染联系

从 Unity 5 开始,使用新的 Standard Shader 作为自然场景的渲染。

阅读官方 Standard Shader 手册 。
选择合适内容,如 Albedo Color and Transparency,寻找合适素材,用博客展示相关效果的呈现

Albedo参数控制表面的基础色调
在这里插入图片描述
官网上由Albedo参数控制的物体颜色例子:
在这里插入图片描述

下面在Unity上做一个简单的练习。

  • 首先通过 Assets -> create material
  • 在新建的 material 的 inspector 面板上修改其 Albedo 值
  • 新建一个 sphere 来展示刚刚做好的 material
  • 将材料拖放到 sphere 上去
  • 按照以上步骤制作五个不同 Albedo 的sphere

结果如下图,可以看到 Albedo 成功改变了物体表面颜色
在这里插入图片描述

A另外,lbedo 颜色的 alpha 通道控制材质的透明度程度。需要注意的是,这只对材质中的Rendering Mode为透明模式(Transparent、Fade)有效,而Opaque模式没有效果。
为上面的sphere添加透明度后效果如下:
在这里插入图片描述

官网上给出的 transparent 的使用样例很棒。镜头可以透过破损的窗口看到建筑物内。玻璃裂开的缝隙是完全透明的,而其余的部分是半透明的。
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值