• 自制Unity小游戏TankHero-2D(4)关卡+小地图图标+碰撞条件分析


    自制Unity小游戏TankHero-2D(4)关卡+小地图图标+碰撞条件分析

    我在做这样一个坦克游戏,是仿照(http://game.kid.qq.com/a/20140221/028931.htm)这个游戏制作的。仅为学习Unity之用。图片大部分是自己画的,少数是从网上搜来的。您可以到我的github页面(https://github.com/bitzhuwei/TankHero-2D)上得到工程源码。

    本篇主要记录关卡解析器、小地图图标和对碰撞的原理的探索,需要耐心分析。

    关卡解析器

    在一个关卡里,敌方坦克应该是一波一波地出现,每波敌人出现多少个,每个敌人是什么类型的坦克、出现在什么位置都应该是可配置的。这需要一个关卡解析器,把如下的文字解析为一个数据结构 Level 。

     1 level
     2 {
     3      tank{0 0} |
     4      tank{0 1} |
     5      tank{0 2} |
     6      tank{0 0} tank{0 1} tank{0 2} |
     7      tank{0 0} tank{0 1} tank{0 2} tank{0 0} tank{0 1} tank{0 2} |
     8      tank{0 0} tank{0 1} tank{0 2} tank{0 0} tank{0 1} tank{0 2} |
     9      tank{0 0} tank{0 1} tank{0 2} tank{0 0} tank{0 1} tank{0 2} tank{0 0} tank{0 1} tank{0 2} |
    10      tank{0 0} tank{0 1} tank{0 2} tank{0 0} tank{0 1} tank{0 2} tank{0 0} tank{0 1} tank{0 2} tank{0 0} tank{0 1} tank{0 2} 
    11 }

    这段文字的意思是,第一波敌人是类型编号为0,出生位置编号为0的1个坦克;第二波敌人是类型编号为0,出生位置编号为1的1个坦克;。。。

    这种东西我喜欢用编译原理解决,因为我在(https://github.com/bitzhuwei/CGCompiler.git)有一个自己写的自动生成词法、语法分析器的工具。关于这个工具的介绍可参考我博客里关于编译原理的文章(在这里搜索"编译器")。

    先总结一下关卡的文法

    1 <Level> ::= "level" "{" <StepList> "}";
    2 <StepList> ::= <Step> <StepList> | null;
    3 <Step> ::= "step" "{" <TankList> "}";
    4 <TankList> ::= <Tank> <TankList> | null;
    5 <Tank> ::= "tank" "{" <TankPrefab> <BornPoint> "}";
    6 <TankPrefab> ::= number;
    7 <BornPoint> ::= number;

    然后用工具生成词法语法解析器代码。

    剩下的就是自己写一下从语法树到数据结构的转换。代码如下。

     1     public static Level GetValue(this SyntaxTree<EnumTokenTypeLevelCompiler,
     2         EnumVTypeLevelCompiler, TreeNodeValueLevelCompiler> syntaxTree)
     3     {
     4         if (syntaxTree == null) { return null; }
     5 
     6         var result = new Level();
     7         _GetLevel(result, syntaxTree);
     8         return result;
     9     }
    10 
    11     private static void _GetLevel(Level result, SyntaxTree<EnumTokenTypeLevelCompiler, EnumVTypeLevelCompiler, TreeNodeValueLevelCompiler> syntaxTree)
    12     {
    13         if (syntaxTree.CandidateFunc == LL1SyntaxParserLevelCompiler.GetFuncParsecase_Level___tail_levelLeave())
    14         {
    15             GetTankList(result, syntaxTree.Children[2]);
    16         }
    17     }
    18 
    19     private static void GetTankList(Level level, SyntaxTree<EnumTokenTypeLevelCompiler, EnumVTypeLevelCompiler, TreeNodeValueLevelCompiler> syntaxTree)
    20     {
    21         if (syntaxTree.CandidateFunc == LL1SyntaxParserLevelCompiler.GetFuncParsecase_TankList___tail_tankLeave()
    22             || syntaxTree.CandidateFunc == LL1SyntaxParserLevelCompiler.GetFuncParsecase_TankList___tail_or_Leave())
    23         {
    24             var egg = GetTank(syntaxTree.Children[0]);
    25             level.Add(egg);
    26             GetTankList(level, syntaxTree.Children[1]);
    27         }
    28         else if (syntaxTree.CandidateFunc == LL1SyntaxParserLevelCompiler.GetFuncParsecase_TankList___tail_rightBrace_Leave())
    29         {
    30             //nothing to do
    31         }
    32 
    33     }
    34 
    35     private static TankEgg GetTank(SyntaxTree<EnumTokenTypeLevelCompiler, EnumVTypeLevelCompiler, TreeNodeValueLevelCompiler> syntaxTree)
    36     {
    37         if (syntaxTree.CandidateFunc == LL1SyntaxParserLevelCompiler.GetFuncParsecase_Tank___tail_tankLeave())
    38         {
    39             var tankPrefab = GetTankPrefab(syntaxTree.Children[2]);
    40             var bornPoint = GetBornPoint(syntaxTree.Children[3]);
    41             var result = new TankEgg(tankPrefab, bornPoint);
    42             return result;
    43         }
    44         else if (syntaxTree.CandidateFunc == LL1SyntaxParserLevelCompiler.GetFuncParsecase_Tank___tail_or_Leave())
    45         {
    46             var result = new TankEgg(-1, -1);
    47             return result;
    48         }
    49 
    50         return null;
    51     }
    52 
    53     private static int GetBornPoint(SyntaxTree<EnumTokenTypeLevelCompiler, EnumVTypeLevelCompiler, TreeNodeValueLevelCompiler> syntaxTree)
    54     {
    55         if (syntaxTree.CandidateFunc == LL1SyntaxParserLevelCompiler.GetFuncParsecase_BornPoint___numberLeave())
    56         {
    57             var result = int.Parse(syntaxTree.Children[0].NodeValue.NodeName);
    58             return result;
    59         }
    60 
    61         return 0;
    62     }
    63 
    64     private static int GetTankPrefab(SyntaxTree<EnumTokenTypeLevelCompiler, EnumVTypeLevelCompiler, TreeNodeValueLevelCompiler> syntaxTree)
    65     {
    66         if (syntaxTree.CandidateFunc == LL1SyntaxParserLevelCompiler.GetFuncParsecase_TankPrefab___numberLeave())
    67         {
    68             var result = int.Parse(syntaxTree.Children[0].NodeValue.NodeName);
    69             return result;
    70         }
    71 
    72         return 0;
    73     }
    GetLevel

    在VS2013里你可以获得Tip,便于coding。

    解析器写好了,调用方式如下。

    语法分析器的类型太长,只好用上图表示一下。

    这样就有敌方坦克一波一波来袭的感觉了。

    小地图图标

    小地图上显示的坦克很不清晰,如果能显示出一个鲜艳的三角形就好了,尖头指向开炮的方向。如下图所示,绿色的为玩家坦克,红色的为敌方坦克。

    首先给坦克的prefab增加一个显示箭头的子对象。

    给子对象smallMapTankHero增加Sprite Renderer组件,在组件的Sprite属性里赋予下面的图片,并把此对象的Layer属性设置为自定义的"smallCamera"(Layer的名字无所谓)。

    Hierarchy里的smallCamera对象是用来显示小地图的。设置其Culling Mask属性如下图所示,勾选smallCamera。这样,箭头就会显示在smallCamera里。

    相应的,在主摄像机Main Camera里,设置Culling Mask属性如下图所示,取消勾选smallCamera。这样,箭头就不会显示在smallCamera里。

    有点平行世界异次元的感觉。

    小地图里仍旧会显示原有的坦克贴图,为了挡住坦克贴图,我们把小地图图标向上(靠近摄像机的方向)移动一点点。

    当然,也可以通过把坦克对象的Layer设置为一个自定义的Layer(比如自定义为realworld),然后在smallCamera的Culling Mask属性中取消勾选realworld即可。不过这还需要把各种对象都放到自定义的Layer里去,太麻烦了。

    碰撞

    Unity中的碰撞,有Collision(物理碰撞)和Trigger(触发器碰撞)两种。

    这里我用精心设计的试验分析出了碰撞的产生条件。

    触发碰撞的基本条件

    下面,假设Hierarchy中有两个对象A和B,A和B都有Collider组件和Rigidbody组件。那么:

    如果去掉其中任何一个的Collider,那么不会发生碰撞事件(只会穿透)。

    如果去掉B的rigidbody,那么移动B去撞A时,不会发生碰撞事件(只会穿透)。

    如果去掉B的rigidbody,那么移动A去撞B时,Collision和Trigger的碰撞都可以发生在A和B身上,但B不会受物理引擎影响而移动。

    OnTriggerXXX的触发原则

    下面,再假设A是Cube,B是sphere;A有3个Box Collider,设置第2个Box Collider的Is Trigger为true;B有4个Shpere Collider,设置第2、3个Sphere Collider的Is Trigger为true。

    然后,给A和B分别添加如下的脚本组件:

     1 public class TriggerTest : MonoBehaviour
     2 {
     3 
     4     // Use this for initialization
     5     void Start()
     6     {
     7     }
     8 
     9     // Update is called once per frame
    10     void Update()
    11     {
    12     }
    13 
    14     void OnTriggerEnter(Collider other)
    15     {
    16         Debug.Log(string.Format("{0} trigger {1}'s({2})", this.name, other.name, other.GetInstanceID()));
    17     }
    18 
    19     void OnCollisionEnter(Collision collision)
    20     {
    21         Debug.Log(string.Format("{0} collision {1}'s({2})", this.name, collision.transform.name, collision.collider.GetInstanceID()));
    22     }
    23 }

    这样就可以记录自己碰撞了对方的哪个Collider。 

    我们先让A撞B,再让B撞A,分别得到如下图左右所示的结果。

    分析发现,虽然引发的碰撞事件的先后顺序有所不同,但碰撞事件是相同的,都是那16个Trigger事件和2个Collision事件。

    据此我总结出Trigger事件的触发原则,如下图所示。

    当A的第2个Collider设置为Is Trigger=true时,此Collider会与B的各个Collider都引发一次Trigger碰撞事件。B也同理。不过两边均为Is Trigger=true时,只会引发一次。数一下上图,可以看到有8条线,每条线在AB两方各引发一次Trigger碰撞事件,所以就是上文的16次。另外,每个Collider引发的次数也与上文的图示吻合。

    OnCollisionXXX的触发条件

    下面,我把A和B的所有Collider的Is Trigger都设为false,来研究Collision的触发条件。

    下图展示了去掉B的Rigidbody后,分别用A撞B和用B撞A的情况。

    可以看到,没有Rigidbody的B撞A,什么都不会发生,B直接穿透了A。

    再用含有2个Collider的Cube和含有2个Collider的Sphere试验(此处略)就可以知道,A撞B则会使A的第1个Collider依次与B的各个Collider引发Collision碰撞事件。所以上图左侧会有1个A的Collider与4个B的Collider引发的Collision事件。

    继续看下图,是同时具有Rigidbody的A和B相互碰撞的结果。

    可以看到A和B都只有1次Collision事件。

    根据上述试验,我认为Unity3D引擎在处理Collision事件时,是去找AB双方的Rigidbody,如果找到了就让它执行物理引擎;如果都找到了,此次碰撞就告结束,不再引发此次A和B的Collision事件。

    总结

    我查了Unity一些资料,总结了一下Unity中关于碰撞的原则:

    physics will not be applied to objects that do not have Rigidbodies attached.
    Kinematic Rigidbody 自身不受外力,但仍旧可对其它物体产生力。

    您可以到我的github页面(https://github.com/bitzhuwei/TankHero-2D)上得到工程源码。

    请多多指教~

  • 相关阅读:
    解决跨操作系统平台JSON中文乱码问题
    httpencode编码
    DELPHI搭建centos开发环境
    cross socket和msgpack的数据序列和还原
    libmidas.so.2
    开发WINDOWS服务程序
    idhttp访问DATASNAP有密码验证的中间件
    接口操作XML
    HttpApplication中的异步线程
    Assembly类
  • 原文地址:https://www.cnblogs.com/bitzhuwei/p/tank-hero-2d-4-level-icon-collision.html
Copyright © 2020-2023  润新知