unity 2d 横版2d 做横版平台游戏有什么好的寻路算法或插件

扫一扫,访问微社区
后使用快捷导航没有帐号?
签到成功!您今天第{todayrank}个签到,签到排名竞争激烈,记得每天都来签到哦!已连续签到:{constant}天,累计签到:{days}天
Unity学习笔记2 简易2D横版RPG游戏制作(二)
阅读&8211 |
十二、敌人受攻击时的闪烁和Player的生命值的修正上一篇中,我们利用Controller2D中的IEnumerator TakenDamage接口,使得我们的Player受到攻击时会进行闪烁,我们同样地也希望在我们的敌人身上可以实现相同的效果。所以我们现在需要复制Controller2D脚本里面的两个内容到我们的Enemy2D脚本里面去:第一个内容:[csharp]&第二个内容:[csharp]&接下来我们需要对Bullet脚本进行处理,使其在碰撞到我们的敌人时,向敌人发送一个闪烁的信号:(修改内容如下)[csharp]&其实就是增加了一句:[csharp]&解决了敌人的闪烁之后,我们接下来要处理另外一个问题了。这个问题就是,我们的角色每消灭一些敌人就会增加经验,而经验的增加会自动补充HP,这个有点不对头和不合理,我们升级的时候增加的应该是最大HP,在每个等级下,我们应该设置一个当前的最大HP,不能无限制地增加HP才对,所以呢,我们需要将原来的GameManager脚本里面的playersHealth变量改名为curHealth,并且增加一个maxHealth变量,同时设置为3,将原本的playersHealth++修改为maxHealth++,这样的话,消灭敌人只会增加你的最大HP,而不会增加你当前的HP,至于升级之后是不是要让HP全满呢,这个功能很容易实现,我暂时不想加进去。接下来我们就需要考虑下一个问题了,那就是角色的战斗力必须随着等级的提升而得到提升才比较合理。不能每次攻击都只扣敌人一滴血啊。而且敌人只有3到6滴血的设定不太合理,其实反正敌人的血是不显示出来的,完全可以设置一些大一点的数值,比如100,500之类的,这个就到后面有需要再改吧。现在暂时先不动。十三、补血药的设置在这一讲里面,首先要处理一个问题:我们的Player应该是有战斗力的,而不是每次只向敌人发送1滴的伤害值。利用某某++的方法可以很容易实现升级时增加战斗力,这个就不说了。关键是怎么弄成用我们的战斗力去减敌人的血值。首先,我们在GameManager脚本里面添加这么一行:[csharp]&然后把我们的curHealth,也就是当前HP的值也改成static public。这里顺便补充一下,static在c#中的作用。静态分配的,有两种情况:
1. 用在类里的属性、方法前面,这样的静态属性与方法不需要创建实例就能访问,
通过类名或对象名都能访问它,静态属性、方法只有“一份”:即如果一个类新建有N个
对象,这N 个对象只有同一个静态属性与方法;
方法内部的:
方法内部的,执行完值不消失,再次执行此对象的方法时,值仍存在,
它不是在栈中分配的,是在静态区分析的, 这是与最大的区别;如果这个说得不具体的话,那么可以看一下下面这个,红黑联盟里面讲的,非常形象,保证一看马上就明白了:(我也是在看视频教程的过程中碰到了static不懂,然后看下面这个理解透彻的)提起static,一般理解为静态、全局。何为static?我理解的static属于程序的直属单位,而非static就是非直属单位。举一个非常常见的例子,中国有4个直辖市,北京、上海、天津、重庆,这些相当于static,而广州、南京、杭州等就是非static,中央可以直接管理北京、上海、天津、重庆,而广州、南京、杭州应由各省政府管理,Main方法可以直接调用static,而调用非static需要实例化。[csharp]&讲的形象就达到目的了,为刚开始学习的同学加把劲儿。原链接:好啦,然后我们继续。接着我们打开我们的Bullet脚本,把里面的内容修改成这样:[csharp]&其实上面的内容只是将[csharp]&这一行进行了处理,我们原本的那个damageValue变量已经被删掉,然后替换成我们GameManager里面的bulletDamage,因为我们的这个bulletDamage已经修改为static,所以现在可以这样调用了。以后如果想要升级增加角色的杀伤力的话,就容易多了。接下来我们想要在运行游戏中按C的时候顺便显示我们的战斗力,这个太简单了,只需要对GameManager里面做一点点修改:[csharp]&这一部分大家可以随便修改,你想显示什么就修改什么。至于在画面中的位置,可以调整GUIStats的transform,这个就不再赘述。我稍微做了点修改。接下来我们需要在场景中新增一个quad,去掉Mesh Collider,加上Box Collider,然后Box&Collider的size全部改成1,transform里面的position的z值别忘了设成0,名字随便起,是用来做成补血药的,我命名为HealthPotion,然后为它增加一个同名的tag。为了区分,我顺便弄了一个同名的material扔上去,将颜色调成粉红色。(呵呵……感觉是毒药而不是血药啊……)接着我们打开上次弄的那个StickToPlatform脚本,把里面的东西复制到我们的Controller2D上面,其实因为两个脚本都是绑定在Player身上,就没必要弄两个脚本了,直接合并成一个就成了。然后我们再增加一段小代码来使得我们的Player碰到带有HealthPotion的物体时,将这个物体摧毁并且curHealth值加1。[csharp]&很简单吧,现在补血药就已经做好了。想要做出补多少血的补血药都已经不是什么问题了。想要做出补蓝、增加战斗力什么的补血药,也不是什么问题了哈哈。有个地方需要大家注意一下,那就是curHealth必须加上static,否则会出现An object reference is required to access non-static member这样的报错显示。在全局静态函数里面是不可以使用非全局静态变量的。吃药前(注意左上角两个红心)吃药后,哈哈,疗程短见效快,一粒补一滴血~关于Box Collider的范围的问题,可以参考上一篇讲到的Player的Character Controller组件的问题,所以我们可以顺便把药物的Box Collider的size的X值改为1.3。十四、游戏暂停和游戏存档游戏存档看似简单,不过也是个比较蛋疼的问题。我们打开GameManager脚本:首先,我们增加一个布尔值:[csharp]&然后我们增添以下代码:[csharp]&然后保存脚本。现在我们在运行游戏的过程中按P键,就可以调出一个这样的画面:当我们点击保存游戏时,就会保存相应的数据。我们可以先杀掉一只小怪,然后保存,然后重新运行,然后点击“显示保存的数据”,这个时候就会看到:但是这个时候有两个问题,第一个问题是,虽然在这里我们可以看到保存的数据,但是实际上在游戏里面,如果我们按C键查看角色的属性时,发现经验值并没有保存下来。第二个问题是,我们的存档还不知道怎么删除……接下来马上解决这个问题:十五、载入游戏和删除存档好了,我们继续修改我们的GameManager脚本,首先我们要增加一个void Awake()函数,这个东西和void Start()有什么不同呢?我顺便摘录了一段网上搬过来的笔记:这是博客园的一篇介绍的原链接:初学者经常把Awake和Start混淆。简单说明一下,Awake在MonoBehavior创建后就立刻调用,Start将在MonoBehavior创建后在该帧Update之前,在该Monobehavior.enabled == true的情况下执行。[javascript]&&我们通常书写的脚本,并不会定义[ExecuteInEditMode]这个Attribute,所以Awake和Start都只有在Runtime中才会执行。例1:[javascript]&&以上代码,在Awake中我们调用了enabled = 禁止了这个MonoBehavior的update。由于Start, Update, PostUpdate等属于runtime行为的一部分,这段代码将使Start不会被调用到。在游戏过程中,若有另外一组代码有如下调用:[javascript]&&这个时候,若该MonoBehavior之前并没有触发过Start函数,将会在这段代码执行后触发。例2:player.cs[javascript]&&other.cs[javascript]&&以上代码中,我们在player Awake的时候去为handAnchor赋值。如果我们将这步操作放在Start里,那么在other.cs中,当执行GetWeapon的时候就会出现handAnchor是null reference.总结:我们尽量将其他Object的reference设置等事情放在Awake处理。然后将这些reference的Object的赋值设置放在Start()中来完成。当MonoBehavior有定义[ExecuteInEditMode]时当我们为MonoBehavior定义了[ExecuteInEditMode]后,我们还需要关心Awake和Start在编辑器中的执行状况。&&& 当该MonoBehavior在编辑器中被赋于给GameObject的时候,Awake, Start 将被执行。&&& 当Play按钮被按下游戏开始以后,Awake, Start 将被执行。&&& 当Play按钮停止后,Awake, Start将再次被执行。&&& 当在编辑器中打开包含有该MonoBehavior的场景的时候,Awake, Start将被执行。值得注意的是,不要用这种方式来设定一些临时变量的存储(private, protected)。因为一旦我们触发Unity3D的代码编译,这些变量所存储的内容将被清为默认值。下面再来看看Unity圣典中的解释。&Awake()当一个脚本实例被载入时Awake被调用。Awake用于在游戏开始之前初始化变量或游戏状态。在脚本整个生命周期内它仅被调用一次.Awake在所有对象被初始化之后调用,所以你可以安全的与其他对象对话或用诸如 GameObject.FindWithTag 这样的函数搜索它们。每个游戏物体上的Awke以随机的顺序被调用。因此,你应该用Awake来设置脚本间的引用,并用Start来传递信息。Awake总是在Start之前被调用。它不能用来执行协同程序。Start()Start仅在Update函数第一次被调用前调用。Start在behaviour的生命周期中只被调用一次。它和Awake的不同是Start只在脚本实例被启用时调用。你可以按需调整延迟初始化代码。Awake总是在Start之前执行。这允许你协调初始化顺序。好了,这样就清楚为什么要用Awake()函数了吧。接下来我们就这样弄,首先我们设置一个int saved,让它等于零。(如果不赋值的话,它也会默认等于零)[csharp]&接下来我们就写下这么一个Awake函数:[csharp]&在运行这个Awake函数的时候,就会让saved先获取我们保存的值。(等一下在下面存档的那部分脚本里面,我们会保存先把saved赋值为1,再进行保存),因为PlayerPrefs不能保存布尔值,所以我们用一个int的0和1来代替就行了,一样的。如果我们的游戏没有存档的话,saved读取不到任何数据,就会默认为零,那么就相当于不会接下去读取我们保存的数据了,反之,如果读取到了1,就相当于读取到了“已经有保存的数据”的情况,就需要继续执行。在保存数据部分,我是这样弄的:[csharp]&我们可以看到,在保存之前,我们先将saved赋值为1,然后再保存,这样的话,当我们重新载入游戏时,就会进行一个是否保存了游戏的判断。至于下面那个“显示保存的数据”的按钮功能,只是我用来debug.log的,没什么用,纯属调试,可以无视。可能有人会说,保存游戏就直接保存,然后直接读取数据不就行了,为什么还要弄一个saved来判断呢?我一开始也是没有弄这个玩意的,后来发现了一个问题,在这里解释一下:假设我们现在删除了游戏存档,而没有这个用来判断是否有存档的saved值的话,那么我们的脚本自然就不管三七二十一,你没有存档它也会当成你是有存档的,这样会出现什么问题呢?这样的话,我们的curEXP = PlayerPrefs.GetInt ("Player EXP");和level = PlayerPrefs.GetInt ("Player Level");这两句话就会得不到任何数据,那么就会默认为零,那么,下面的maxEXP = level * 50;还有maxHealth = level + 2;这两句的计算就肯定会出问题了。maxEXP会变成0,而maxHealth会变成2,最大经验值变成零也就算了,我们的血值还从3变成了2,这不是坑爹么……哈哈,所以,这下子明白为什么要做一个是否saved的判断了吧。对于我这样的小游戏来说,就已经需要制作一个是否saved的判断了,对于需要保存大量数据的大游戏来说那就更是如此。希望大家如果是制作RPG类型的游戏的话,也可以养成类似的习惯。接着要解决删档的问题。这个也是RPG游戏的一个重点内容。我们打开MainMenu脚本,然后在if (showGUIOutline)里面加入以下内容:[csharp]&其实主要就是添加一个PlayerPrefs.DeleteAll();而已,没什么复杂的。当然,为了方便我们在窗口中调整GUI的位置,我们也增加了guiPlacementX3还有guiPlacementY3这两个public的float值。这里就没必要贴出来了。接着在Mainmenu场景里面的Maincamera上面调整好三个GUI按钮的位置:接着我们来测试运行一下,我们进入Mainmenu场景,然后运行,点击“删除存档”按钮:我们会看到print出了一句已删除存档的提示。接着我们载入游戏,来到我们那个丑丑的游戏场景,然后按C,查看一下我们当前的各项状态。现在我们还是初始状态,我们去随便刷掉两只怪,然后顺便去作死一下,扣掉一滴血。然后按P键调出保存菜单。现在我已经按了保存,然后我按了“显示保存的数据”按钮,现在我们可以从右边的Console列表里面看到我们保存的数据。好了,我们退出,重新进来~,再查看一下:细心一点的朋友应该注意到,这次稍微有了点不同。那就是我又恢复成3滴血了。这是因为我们重新加载的时候,curHealth会变成当前等级的maxHealth,所以我们重新载入之后不是2滴血,而是3滴血。现在我们重新进入Mainmenu场景,然后删除存档,再重新进来一遍:可以看到,我们的角色的全部资料都清零了。(右手边的Console显示了我刚刚有进行“删除存档”按钮的点击操作,没有造假,哈哈)好了,现在我们已经解决了角色存档的问题。这部分可能自己实际操作的时候会碰到一些问题,需要大家多做几次,特别是不同的游戏,情况肯定不一样,这个没办法有统一的标准,这里只是提供一个思路。十六、自动存档前面我们提到了存档的方法了。有些游戏是即时存档的,就是在Update每一帧都进行一次存档的操作,对于小游戏来说,这种做法无可厚非,但是对于类似宠物小精灵这种有庞大数据的游戏来讲,即时存档是不太可行的。也许有其他优化的方法,比如在独立游戏Terraria中,它就是支持大数据即时存档的,目前我尚不知道这种方法要如何实现。下面,我们可以利用类似技能冷却的原理,设置一个定时自动存档器,比如说每隔五秒钟或者十秒钟自行存档一次,为了让玩家知道有自动存档的情况,我们可以调用一些GUI来显示(我这里就直接print了)。同理,我们也可以在场景中布置一些特殊物体,进行自动的存档,比如说某一个关卡末尾的大门,我们的Player碰到大门之后就会跳到下一个关卡,同时自动保存我们Player身上的全部数据。(至于这个场景跳转门或者说是位置跳转门能不能双向跳动,这个就要看你的游戏是怎么布置的了。)废话不多说,马上开始:首先我们需要在GameManager脚本里面创建一个叫做SaveGame()的函数,然后改为public类型(为什么要这样做后面会解释),然后把我们前面在if(pauseMenu)里面的部分内容移到这个函数里面去:[csharp]&只有加上public,才能在外部进行调用嘛。好了,现在我们增加下面的内容:[csharp]&想要跳到下一个关卡有个很简单的函数可以使用,就是Application.LoadLevel(),可是有个问题,就是如果我们直接选择加载某一个关卡的话,我们这个脚本就不能重复利用了。也就是说,我们想要做成一个只要一碰到就会自动跳转到下一关的功能的脚本,这样就不必在每一个关卡里面都来写一个特定的脚本。首先,我们用string thisLevel = Application.loadedLevelN这句话获取当前关卡的名字,我们之前将关卡命名为Scene1,现在直接改成1就行了。这样的话,我们就相当于得到了关卡的序列号。理论上来讲,我们只需要Application.LoadLevel(thisLevel+1);就应该是可以跳转到下一个关卡的了。如果这样的话那就很方便了。可是实际上有个很大的问题,那就是关卡的名字是字符串,字符串可不能直接做加减法的,所以我们需要将字符串强制转换为int类型。C#里面有Convert.ToInt32的转换方法,但是我在unity里面没办法使用(难道是我的打开方式不对?)所以我采用了另一种方法,也就是上面脚本的int intThisLevel = int.Parse(thisLevel);这样,我们获取当前的字符串名称1就会变成整数1(为了实现这个功能,我们必须将除了Mainmenu关卡之外的其他关卡全部命名为阿拉伯数字名称,即1、2、3……)然后我们对这个整数1进行加1的处理,即:int intNextLevel = intThisLevel+1;接着,我们将得到的这个新的整数重新转换为字符串string nextLevel = intNextLevel.ToString();到这里,加载下一个关卡的任务就完成了,最后我们加上这么一句:Application.LoadLevel(nextLevel);即可跳转到下一关卡。关于强制类型转换的,可以参考这个:C#,int转成string,string转成int1,int转成string用toString&或者Convert.toString()如下&例如:int varInt = 1;&string varString = Convert.ToString(varInt);&string varString2 = varInt.ToString();2,string转成int如果确定字符串中是可以转成数字的字符,可以用int.Parse(string s),该语句返回的是转换得到的int值;如果不能确定字符串是否可以转成数字,可以用int.TryParse(string s, out int result),该语句返回的是bool值,指示转换操作是否成功,参数result是存放转换结果的变量。例如:string str = string.Estr = "123";int result=int.Parse(str);string str = string.Estr = "xyz";int.TryParse(str, out result);接下来处理下一个问题,我们在刚刚的if (other.tag == "Door") {}里面插入一句gameManager.SaveGame();当然,在此之前,我们需要在这个脚本里面加上这么一行:[csharp]&这个应该很好理解,就不再赘述了。使用public的原因是为了在外部进行拖拽操作,如果不将其设置为public的话,就会出现NullReferenceException: Object reference not set to an instance of an object字样的报错,这个我们在上一篇学习笔记里面已经有提到过了。出现这个的原因是因为我们此处引用了GameManager里面的一个SaveGame()函数,但是unity并不知道你到底在用哪个GameManager,所以会报Null,最简单的解决方法就是拖拽大法,也可以用GetComponent&&的方法。看下面,这个是我的Player身上的Controller2D脚本的设置,我已经把GameManager物体拖放到相应位置了。刚开始的时候,原作者并没有使用这个方法,而是直接复制了Bullet脚本里面的other.gameObject.SendMessage("TakenDamage",SendMessageOptions.DontRequireReceiver);这句进行修改。现在我们顺便来想想为什么不可以采用这种方法。因为这种方法是对着other.gameObject发送信号,而我们是对着自己发送信号啊。可能有人会说,那还不简单,我们直接把other.gameObject.删掉不就可以了吗?原视频的作者也试了一次,不行。但是他没解释。我想了一段时间,终于明白了。因为SendMessage不可以把内容发送给自己这个脚本。发送给同个物体身上的不同脚本还是行得通的,但是自己发送给自己就不行了。所以,这种方法是行不通的,就只能用上面提到的那种方法了。现在我们已经实现了场景跳转和按键存档功能,接下来就是自动存档功能了。我们在GameManager脚本的Update函数里面进行如下增添:[csharp]&上面的autoSaveTimer是自定义的一个int变量,我顺便设置成public,方便在外面修改。我将它改为10,也就是每隔10秒就会自动保存一次,前面我们将保存的功能整合成一个SaveGame()函数就是为了这个目的。那句print(“保存啦~~”)只是用来卖萌和提示自己的,嘿嘿~每次保存之后autoSaveTimer这个自动保存的计时器就会清零,然后重新进入下一轮的计时,这样就可以实现循环保存的功效了。因为我们的游戏数据比较小,所以可以每隔一段时间就自动保存一次,大数据的游戏就不建议这样弄了。这一讲很长,也涉及到了很多问题。所以我花了很长时间整理。如果大家有什么不太清楚的话,最好多看几遍,然后自己再试着操作一下试试。当然,这个是针对新手说的,各路大神可以无视哈。十七、关卡选择在这一讲里面,我们首先创建一个新的场景(为了方便测试,我把上一次弄的场景2直接重命名为场景LevelSelect,你要新建一个当然也没问题,这个不是重点,哈哈),这个场景的作用就是在进入时可以用来选择进入各个关卡的。很多横版的闯关游戏由于关卡比较多,所以在游戏开始的时候都会有这样的选择画面,让玩家自行选择这次要玩哪一关,就不必要每次都从第一关开始玩起了。而且由于Unity在游戏保存这一块非常给力,所以我们完全不需要担心数据丢失方面的问题。接着我们先点击进入我们的Mainmenu场景,在摄像机的MainMenu脚本里面,把Level里面原本的Scene1改成现在的LevelSelect,这样我们就可以通过点击按钮直接跳转到我们现在要用到的这个LevelSelect场景了。接着进入这个场景,然后创建一个同名脚本扔到摄像机上面,然后我们开始编辑:[csharp]&内容很简单,我就不做解释了。前面两个int是因为觉得打screen.width和screen.height很麻烦,所以才那样弄的,至于那个public string Level,之前介绍过了就不再解释为什么要这样做了。我这里只是随便弄了一个出来,大家可以在这个LevelSelect的脚本里面设置一堆不同的按钮,每个按钮连接到你的各个不同关卡里面去,这样就达到了关卡选择的效果啦。接下来看看我们之前的Controller2D脚本,我们在这个脚本里面有这么一段:[csharp]&看了原作者的视频,他是打算做一个叫做Door的脚本,把上面的那部分功能转移到这个脚本里面去,然后再去把这个东西拉到我们的Door物体上,其实这一步可做可不做。我想来想去,这么做的好处大概就是可以为每一个Door弄一个public string Level,然后设置那些穿越什么的方便一些吧。视频里面的Door脚本里面的GameManager脚本还是采用将它改为public,然后再在外面进行拖拽的方法。那么有没有什么方法可以不进行这些拖拽呢?当然可以的啊。只要利用GetComponent就可以了。下面贴出我写的脚本:[csharp]&我们首先要定义一个GameObject来利用FindGameObjectWithTag得到它(使用这种方法需要增加tag,其实和拖拽方法大同小异,这个就看个人喜好了。两种方法大家都可以使用的)当然,我们要给GameManager加上一个同名的tag。接着我们就利用这个:gameManager = gameObject.GetComponent&GameManager&();很简单,我就不解释了,不过需要注意一下格式。如果在函数上有什么不太明白的,建议大家自己去查查手册。链接:另外呢,在实际操作过程中,如果大家按照我上面的脚本照抄一遍,就会出现这样的黄字警告:Assets/Scripts/Door.cs(8,16): warning CS0108: `Door.gameObject' hides inherited member `UnityEngine.Component.gameObject'. Use the new keyword if hiding was intended&这到底是怎么回事呢?一开始我真的想不明白,用百度和Google也找不到解答。后来自己想了想,然后又查了一下词典,总算明白到底是怎么一回事了。我们都知道,黄字警告一般是不会影响游戏的运行的,但是肯定是有某些不太合理的地方,所以我们的Unity会非常人性化地给我们提个醒。我经常碰到的黄字提醒一般都是定义了某个变量,但是从未使用过。而这次碰到的这个情况,是脚本里面的gameObject变量和系统里面本身自带的gameObject实例名称重复了。因为我们知道,gameObject是个关键词,所以如果我起了个这样的名字的话,虽然是可以运行的,但是Unity也许容易混淆,所以它就会给我们一个这样的提醒。这里也顺便提醒一下大家,在定义变量的时候尽量不要使用和系统自定义的关键词重复的东西。否则可能会出现比较严重的问题。上面的这个问题,我们只需要把gameObject改成gameObjectGM,就不会报错了。好了,那么这一讲想要实现的功能就都实现了。我们可以接着下一讲的内容了:(顺便说一下,我不是很喜欢unity4.3版本里面自带的monodevelop脚本编辑器,所以我改成了VS2013,这篇写完之后我会写一篇替换编辑器的内容。)这是我处理过的VS2013的界面,感觉还是挺和谐的嘻嘻。下一篇我们再聊这个。由于排版方面出了很奇怪的问题,所以第二篇就只能先到这里结束了。大概是因为我弄了太多天的同一篇,又有些东西是复制进来的,导致排版的时候总是出错吧。先这样吧,下一篇我会将没有说完的补完。unity3d 2d横版
作者的其他最新博客
评论 ( 个评论)1 / 16 椤

我要回帖

更多关于 unity2d横版游戏教程 的文章

 

随机推荐