百纳科技 审校
北京
图书在版编目(CIP)数据
Unity游戏案例开发大全/吴亚峰,杜化美,于复兴编著.--北京:人民邮电出版社,2015.1
ISBN 978-7-115-37252-9
Ⅰ.①U… Ⅱ.①吴…②杜…③于… Ⅲ.①游戏程序—程序设计 Ⅳ.①TP311.5
中国版本图书馆CIP数据核字(2014)第243190号
内容提要
随着智能手机的普及,一些可玩性强的手机游戏应用也逐渐普及开来。本书结合作者多年从事游戏应用开发的经验,详细地介绍了10款Unity 3D游戏案例的开发,主要内容如下。
第1章——Unity 3D 基础以及开发环境的搭建,简要介绍了Unity 3D 的诞生、特点、开发环境的搭建以及运行机制;第2章——3D 极品桌球,3D 极品桌球使用了着色器,极大地丰富了游戏的视觉效果,增强了用户体验,桌球运动十分真实酷炫;第3章——3D 迷宫墨盒,使用了Unity 3D 强大的物理引擎,配合重力感应增强了用户体验,滚球运动十分真实;第4章——穿越子午线,借助火热的界面搭建插件NGUI,结合触摸技术,再加上智能AI,使玩家得到真实体验;第5章——古墓推金币,玩家在游戏中将能够体验到3D技术与物理引擎带来的真实的视觉享受;第6章——可乐可乐,玩家通过滑动触摸屏进行发球;第7章——坦克大战,模拟了坦克射击场景;第8章——小狗快跑,本游戏充分体现出酷跑类游戏快速的游戏节奏,能够充分发挥玩家的反应能力;第9章——3D 虚拟停车场,用户在玩游戏时,不但体验了3D 技术与物理引擎带来的真实的视觉享受,还能获得一些停车技巧;第10章——拯救蘑菇村,玩家通过触摸屏幕上的摇杆或者各个按钮,实现操控飞机等效果;第11章——百纳赛车,这款游戏可以让人们随时体验驾驶赛车所事来的乐趣。
同时,为了便于读者的学习,本书附赠了光盘,其中包含了书中所有案例的完整代码,能更好地帮助读者快速掌握相应的开发技术。本书适合有一定基础、有志于游戏开发的读者学习使用,也可以作为相关培训学校和大专院校相关专业的教学用书。
◆编著 吴亚峰 杜化美 于复兴
审校 百纳科技
责任编辑 张涛
责任印制 彭志环
◆人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
网址 http://www.ptpress.com.cn
北京昌平百善印刷厂印刷
◆开本:787×1092 1/16
印张:31 彩插:4
字数:822千字 2015年1月第1版
印数:1-3500册 2015年1月北京第1次印刷
定价:79.00元(附光盘)
读者服务热线:(010)81055410 印装质量热线:(010)81055316
反盗版热线:(010)81055315
广告经营许可证:京崇工商广字第0021号
为什么要写这样的一本书
近几年来,Android、iOS平台游戏以及Web网页游戏发展迅猛,已然成为带动游戏行业发展的新动力。遗憾的是,目前除了一些成功作品外,很多的游戏都属于宣传攻势大于内容品质的平庸之作。面对这种局面,3D游戏成为独辟蹊径的选择。但是传统的3D游戏开发有门槛高、成本高的问题,中小公司一般难以切入。而Unity 3D 引擎的出现大大改善了这一情况。
Unity 3D 是由Unity Technologies 开发的一款可以方便地开发3D 游戏、建筑可视化、实时交互式三维动画的3D引擎。通过Unity 3D能方便地创造高质量的3D游戏和非常真实的视觉效果,这降低了开发3D游戏的门槛与成本。
由于最近几年Unity 3D的迅猛发展,该游戏引擎通过不断地优化与改进已经升级到4.3版本。在Unity 4.3 中增加了许多新的特性,如全新的动画系统、支持移动平台的实时阴影、最新的状态机技术等。本书案例也随着该游戏引擎的升级加入了许多新的内容,希望对不同学习层次的读者都有所帮助。
本书通过对Unity 3D 集成开发环境的搭建以及对10 个游戏案例进行实战介绍,给读者一个由浅入深、循序渐进的学习过程,相信每一位读者都会通过本书得到意想不到的收获。
经过近一年见缝插针式的奋战,本书终于完成了。回顾写书的这半年多时间,不禁为自己能最终完成这个耗时费力的“大制作”而感到欣慰。同时也为自己能将从事游戏开发近 10 年来积累的宝贵经验以及编程感悟分享给正在开发阵线上埋头苦干的广大编程人员而感到高兴。
本书特点
1.内容丰富,由浅入深
本书内容覆盖了从学习 Unity 3D 必知必会的基础知识,到基于着色器语言所实现的高级特效。这样的内容组织能使初学者一步一步地成长为3D开发的达人,符合绝大部分想学习3D开发的学生与技术人员以及正在学习3D开发人员的需求。
2.结构清晰,讲解到位
本书案例在讲解时每一具体步骤都给出了丰富的插图以及注意要点,使得初学者易于上手,有一定基础的读者便于深入。书中所有的案例均是根据笔者多年的开发心得进行设计的,结构清晰明朗,便于读者进行学习与参考。同时书中还给出了很多笔者多年来积累的编程技巧及心得,具有很高的参考价值。
3.实用的光盘内容
为了便于读者学习,本书附赠的光盘中包含了书中所有案例的完整源代码,读者可以直接导入运行,仔细体会其效果,能最大限度地帮助读者快速掌握开发技术。
内容导读
本书共分为11章,其中,第1章介绍了基本开发环境的搭建,后面的第2~11章都给出了一个具体的游戏案例,涵盖了多种不同类型的游戏,主要内容如下。
本书案例所涉及的知识丰富,从基本知识到高级特效以及Unity 3D 强大的物理引擎,适合不同需求、不同水平层次的各类读者。
● 初学Unity 3D 应用开发的读者。
本书中案例涉及大量Unity 3D开发的基础知识,配合本书附赠光盘中所有案例的完整源代码,非常适合初学者学习,使初学者能够最终成为Unity 3D 游戏应用开发达人。
● 有一定3D 开发基础的读者,可以进一步深入学习Unity 3D 高级开发技术。
本书中案例不仅使用了Unity 3D 开发的基础知识,同时也使用了基于着色器语言、关节、动画等技术所实现的高级特效,以及Unity 3D 强大的物理引擎,有利于有一定基础的开发人员进一步提高开发水平与能力。
● 跨平台的3D开发人员。
由于Unity 3D 是跨平台的,可以开发基于多个不同平台的3D 游戏应用项目,因此,非常适合跨平台的3D开发人员。
本书作者
吴亚峰,毕业于北京邮电大学,后留学澳大利亚卧龙岗大学并取得硕士学位。1998年开始从事Java 应用的开发,有10 多年的Java 开发与培训经验。主要的研究方向为OpenGL ES、手机游戏、Java EE以及搜索引擎。同时为手机游戏、Java EE独立软件开发工程师,并兼任百纳科技Java培训中心首席培训师。近 10 年来为多家著名企业培养了上千名高级软件开发人员,曾编写过《Unity 3D 游戏开发技术详解与典型案例》、《Unity 4 3D 开发实战详解》、《OpenGL ES 2.0 游戏开发(上下卷)》、《Android 3D 游戏开发技术宝典——OpenGL ES 2.0》、《Cocos2d-X 案例开发大全》、《Android游戏开发大全》等多本畅销书。2008年初开始关注Android平台下的3D应用开发,并开发出一系列优秀的Android应用程序与3D游戏。
杜化美,西安电子科技大学硕士,有多年的Java程序开发与培训经验。曾参与两项国家自然科学基金项目,在国内外刊物上发表论文 10 余篇。同时兼任嵌入式独立软件工程师,在软件领域有8年的从业经验,最近3年致力于Android嵌入式系统的研究,同时参与开发了多款手机3D游戏应用。
于复兴,北京科技大学硕士,从业于计算机软件领域 10 年,在软件开发和计算机教学方面有着丰富的经验。工作期间曾主持科研项目“PSP流量可视化检测系统研究与实现”,主持研发了多项省、市级项目,同时为多家单位设计开发了管理信息系统,并在各种科技刊物上发表多篇相关论文。2008年开始关注Android平台下的应用开发,参与开发了多款手机3D游戏应用。
本书在编写过程中得到了唐山百纳科技有限公司Java培训中心的大力支持,同时,佘伟伟、代其祥、蒋科、金亮、赵坤、刘喆、陈泽鑫、汪博文、倪文帅以及作者的家人为本书的编写提供了很多帮助,在此表示衷心的感谢!
由于编者的水平和学识有限,且书中涉及的知识较多,难免有错误疏漏之处,敬请广大读者批评指正,同时希望广大读者多提宝贵意见。本书责任编辑联系邮箱为:zhangtao@ptpress.com.cn。
编者
随着手持式终端的快速推广与发展,人们开始逐渐习惯于在手持设备上寻求乐趣,加之一系列物理引擎对手持设备的支持,移动终端模拟现实不再是遥不可及。
本章的游戏《可乐可乐》模拟现实生活中的砸罐子游戏,是一款使用 Unity 3D 游戏开发引擎制作的基于Android平台的益智休闲类游戏。接下来将对游戏的背景和功能以及开发流程逐一进行介绍。
这一节将主要介绍本游戏的背景和功能,让读者对本游戏有一个整体的了解。同时希望通过本节的学习,读者将对本游戏所达到的效果和所实现的功能有一个直观的了解。
近年来,休闲益智游戏开始慢慢流行起来,这类游戏的共同特点是没有过于复杂的规则,上手简单但每关设计都非常有逻辑性与可玩性,可以随时掏出来享受游戏的乐趣,所以深得市面上玩家的青睐。这类游戏的代表作品主要有《愤怒的小鸟》、《指划射门》等,如图6-1和图6-2所示。
《可乐可乐》是使用当前最为流行的Unity 3D 开发工具,结合智能手机的触摸技术打造的一款小型休闲益智手机游戏。玩家通过滑动触摸屏进行发球,在尽量节省球的条件下砸倒更多可乐罐,游戏没有过高的难度,但是依旧具有很高的游戏性,可以锻炼玩家的逻辑思维能力,给予玩家很好的游戏体验。
前一个小节简单地介绍了本游戏的开发背景,本小节将对该游戏的主要功能进行简单的介绍。
(1)单击桌面上的游戏图标运行游戏,首先进入的是欢迎界面,如图6-3所示。
(2)欢迎界面结束后,来到游戏的主菜单界面,如图6-4所示。主菜单界面是本游戏的中转站,联通所有其他场景,玩家可以在本场景中通过单击不同的功能按钮进行界面跳转。
(3)在主菜单界面单击下方的“设置”按钮,或者将屏幕向右滑动,会进入游戏设置界面,在设置界面中有游戏音乐按钮,玩家通过单击该按钮可以设置游戏中背景音乐的开关,在本界面中还包含有游戏开发团队的信息,如图6-5所示。
(4)在主菜单界面单击“开始游戏”按钮,或者将屏幕向左滑动,会进入游戏选关界面,如图6-6所示。在本界面上方有一系列选关按钮,玩家可以单击选关界面下方的箭头或者向右滑动屏幕回到游戏主菜单界面。
(5)在选关界面有一系列选关按钮,每个选关按钮下方的星星板上都显示了玩家在该关卡获得的星星数量。玩家单击不同的关卡图标可以进入相应的关卡,本游戏为玩家设置了4种各具特色的关卡,关卡一如图6-7所示。
(6)在游戏关卡中,下方是供玩家控制的球,前方是可乐罐,玩家通过滑动屏幕发球去撞击平台上的可乐罐,当球用尽或罐子全部被击倒游戏结束。单击右上角的控制板向下拖动或单击暂停按钮,会显示控制板的全部内容。在这里可以控制游戏中声音的开关,如图6-8所示。
(7)在主界面中单击“退出”按钮,退出游戏。
前一节简单介绍了游戏的背景和功能,本节主要介绍本游戏的策划及正式开发前的一些准备工作。游戏开发需要做的准备工作大体上包括游戏策划、美工需求、音乐等。游戏开发前的充分准备可以保证开发人员有一个顺畅的开发流程,保证开发顺利进行。
本小节将对游戏的策划进行简单的介绍,通过本小节的介绍,读者将对本游戏的基本开发流程和方法有一个基本的了解。在实际的游戏开发过程中还需更细致、更具体、更全面的策划,该游戏的策划如下所列。
□ 游戏类型
本游戏是使用Unity 3D 游戏开发引擎作为开发工具,并且以C#脚本作为开发语言开发的一款休闲益智类游戏。本游戏大量使用物理引擎,使游戏中的碰撞、爆炸等效果十分真实,配合炫丽的粒子系统与贴图使得游戏的最终效果相当华丽。
□ 运行目标平台
本例运行平台为Android 2.0 或者更高的版本。
□ 目标受众
可乐可乐规则简单,界面感染力强,采用了手持设备为载体,玩家可以随时随地体验本游戏带来的乐趣。可乐可乐操作感强,后期关卡有一定难度,适合全年龄段现象进行游戏,本游戏可以在娱乐中锻炼玩家的反应能力、逻辑思维能力等。
□ 操作方式
玩家可以通过单击球后滑动屏幕来发球,系统会自动根据滑动的快慢与方向来控制发球力度和发球方向,玩家的目标是在尽量使用最少球的前提下击倒更多可乐罐,当可乐罐全部被击倒时游戏胜利,若将球用尽时仍有可乐罐剩余在平台上则游戏失败。
□ 呈现技术
本游戏采用Unity 3D 游戏开发引擎制作。游戏场景具有很强的立体感和逼真的光影效果以及真实的物理碰撞,同时在绘制方面使用了着色器技术,配合粒子系统和精美的模型,玩家将在游戏中获得炫丽真实的视觉体验。
前一小节介绍了游戏的策划。本小节将介绍一些开发前的准备工作,包括图片、声音等资源的选择与制作,其详细步骤如下。
(1)介绍的是本游戏中所用到的图片资源,将所有按钮图片资源全部放在项目文件Assets\PIC文件夹下。详细情况如表6-1所示。
(2)介绍的是本游戏中所用到的数字纹理图片资源,将所有纹理和背景图片资源全部放在项目文件Assets/PIC/num文件夹下。详细情况如表6-2所示。
(3)本游戏中添加了声音,这样使游戏更加真实。需要将声音资源放在项目目录中的Assets/Sound文件夹下,其详细情况如表6-3所示。
(4)本游戏用的3D 模型是通过3ds Max生成的FBX 文件,然后导入Unity的。而生成的FBX文件需要放在项目目录中的Assets/FBX文件夹下。其详细情况如表6-4所示。
在6.2 小节介绍了游戏开发前的策划和准备工作。本节将简单介绍游戏的架构。读者通过这一节可以进一步了解游戏的开发思路,对整个开发过程也会更加熟悉。
在Unity中,场景开发是开发游戏的主要工作。每个场景包含了多个游戏对象,其中某些对象还被附加了特定功能的脚本。本游戏包含了5个场景,接下来对这几个场景中重要游戏对象上挂载的脚本进行简要的介绍。
□ 主菜单场景
主菜单场景“Zhujiemian”是转向各个场景的中心场景,在该场景中可以通过单击不同的功能按钮进入其他界面,如游戏界面、设置界面、选关界面等。
该场景中的主摄像机挂载了脚本“MyMainMenu.cs”,其主要功能是响应玩家的触摸事件,如单击按钮、滑动屏幕等。每个选关按钮上都挂载了脚本“Choose.cs”,该脚本的主要功能是在选关按钮下方的星星板上绘制本关已经获得的星星。
□ 游戏关卡场景
本游戏中包含四个游戏关卡,分别对应四个游戏场景:“Level1”、“Level2”、“Level3”、“Level4”。每个场景包含的脚本和设置基本相似,仅有罐子的种类和摆放方式不同。下面对游戏关卡场景中个别重要的游戏对象进行详细介绍。
(1)主摄像机“Main Camera”用于在屏幕上呈现游戏画面,该游戏对象上挂载了脚本“GameRule.cs”、“TiShi.cs”。“GameRule.cs”用于判断游戏是否结束,并根据胜负情况生成相应的分数板。“TiShi.cs”脚本的功能是在每关开始前根据本关特色显示游戏提示板。
(2)每个木球游戏对象上都挂载了脚本“Ball.cs”、“LiZi.cs”,“Ball.cs”脚本实现玩家滑动屏幕发球的功能,“LiZi.cs”脚本主要功能包含球撞击时显示粒子系统、播放音效以及实现撞击特殊罐子触发的事件。
(3)每个可乐罐游戏对象都挂载了“GuanZi.cs”脚本,该脚本的主要功能是当罐子被球撞下平台时记录下位置,并在该位置的上方显示加分动画并进行加分,动画播放完后自动删除该罐子则可以达到节省游戏资源的目的。
(4)控制板Ban对象挂载了脚本“GameControlPanel.cs”,该脚本的主要功能是获取玩家手指的触摸位置和触摸方式(单击或滑动)并通过 3D 拾取技术实现按钮的触摸监听,如暂停游戏、回复游戏、游戏静音、返回主菜单重新开始等。
(5)游戏结束后,在主摄像机上挂载的“GameRule.cs”脚本作用下生成分数板,分数板上挂载了脚本“ScoreBoard.cs”,该脚本的主要功能是等到游戏结束后计算本局游戏得分与获得的星星数量,将其显示在分数板上并储存。
上一小节已经简单介绍了游戏的主要场景和使用到的相关脚本,为加深读者理解,这一小节将介绍一下游戏的整体架构。本游戏中使用了很多脚本,接下来将按照程序运行的顺序介绍脚本的作用以及游戏的整体框架,具体步骤如下。
(1)单击游戏图标进入游戏后,经过加载首先来到了游戏的主菜单场景 Zhujiemian,主菜单场景的主摄像机被激活,玩家滑动或单击屏幕时,会触发主摄像机上的MyMainMenu.cs脚本,通过3D拾取技术响应玩家的操作。
(2)单击“开始游戏”按钮,或者手指向左滑动,会进入游戏的选关界面,在选关界面有一系列选关按钮,分别代表本游戏的所有游戏关卡,选关按钮上挂载的 Choose.cs 脚本在各关卡分数板上绘制对应关卡获得的最多星星数。单击不同的关卡按钮会进入相应的游戏关卡场景。
(3)单击关卡一,进入游戏场景“Level1”,主摄像机被激活,当玩家点中木球并向上滑动屏幕时,球上挂载的脚本Ball.cs会将手指滑动位移转换为一个带方向的力并施加在球上。球在飞行中的撞击事件由脚本LiZi.cs处理,负责在撞击点生成粒子系统,播放音效以及撞击特殊罐子的处理办法等。
(4)当球撞倒可乐罐,可乐罐落下平台会撞到下方的“DiBan”触发器游戏对象,罐子上的脚本“GuanZi.cs”激活,该脚本用于播放加分动画并进行加分,同时当玩家看不到可乐罐时删除可乐罐游戏对象达到节省游戏资源的目的。
(5)当玩家单击左上角的暂停按钮,或点中控制板向下拖拉时,控制板上的GameControlPanel.cs脚本激活,使控制板向下滑动,显示出继续、退出、静音、重新开始功能按钮,并关闭Ball.cs脚本,使玩家暂时无法发球。单击继续按钮,控制板上移,并重新开启Ball.cs脚本。
(6)在游戏进行过程中,主摄像机上挂载的“GameRule.cs”脚本始终根据游戏规则判断游戏是否结束,在结束时根据游戏的胜负情况生成相应的分数板,并计算本局游戏的得分和获得的星星数量,在分数板上绘制呈现。
(7)生成分数板后,分数板上挂载的脚本ScoreBoard.cs启用,该脚本将分数转换为纹理贴图渲染到分数板上并人为控制渲染顺序,防止深度检测失败。同时该脚本还实现了分数板上退出、重新开始、进入下一关按钮的按键监听。
(8)当进入某些特殊关卡(如关卡二和关卡三)时,有一些具有特殊功能的罐子。这时主摄像机上挂载的脚本“TiShi.cs”会在游戏开始时生成提示板,提醒玩家该关卡中这些特殊罐子的作用。
前一小节介绍了游戏的整体架构,从本节开始将介绍本案例各个场景的开发,首先介绍本案例的主菜单场景,该场景在游戏开始时呈现,控制所有界面之间的跳转。本节将在前面介绍的基础上对此场景的开发细节进行进一步的介绍。
场景搭建主要是针对游戏地图、灯光、天空盒等环境因素的设置。通过本小节学习,读者将会了解到如何构建出一个基本的游戏世界,接下来将具体介绍场景的搭建步骤。
(1)新建一个场景作为主菜单场景,具体步骤为“File”→“New Scene”,如图6-9 所示。单击File 选项中的“Save Scene”选项,或者使用保存快捷键“Ctrl+C”,在保存对话框中将场景名重命名为“Zhujiemian”。
(2)设置游戏中的环境光。环境光指的是在游戏场景中除去灯光后环境的亮度或颜色,具体步骤为单击“Edit”→“Render Setting”,单击属性查看器中的“Ambient Light”设置环境光的颜色和亮度,在这里选择白色,其他参数设置如图6-10所示。
(3)创建主菜单背景。按步骤“GameObject”→“Creat Other”→“Plane”创建出一个平面并重命名为“beijing”作为主菜单的背景。调整其大小和位置,并将纹理图“zhucaidan.png”拖拉到此对象上。效果如图6-11所示。
(4)选中“beijing”游戏对象,按步骤单击工具栏的“Component”→“Physic”→“MeshCollider”为背景游戏对象添加网格碰撞器。并将该对象勾选为“Static”静态的,使该背景屏幕不参加运动。参数设置如图6-12所示。
(5)为该对象指定层,在游戏中通过层的概念可以轻松地管理一批相同性质的对象。选中“beijing”游戏对象,在属性面板中单击“Layer”下拉列表,选择“Add Layer…”新建层 beijing,并将该游戏对象设置为“beijing”层,如图6-13所示。
(6)创建游戏名称板。将模型“GameName.fbx”导入并从资源列表中拖拉到游戏场景中,自动生成GameName游戏对象。调整其大小和位置并将图片“youximing.png”拖拉到该游戏对象上,如图6-14所示。
(7)选中“GameName”游戏对象,按步骤单击工具栏的“Component”→“Physic”→“Rigidbody”、“Component”→“Physic”→“BoxCollider”,分别为游戏名称板游戏对象添加刚体和盒子碰撞器。组件列表如图6-15所示。
(8)下面是游戏开始按钮的创建。将模型“GameStart.fbx”导入并拖曳到游戏场景中,生成“GameStart”游戏对象。调整其大小和位置后并将图片“youxikaishi.png”拖到该游戏对象上。最终效果如图6-16所示。
(9)为游戏开始按钮对象添加刚体和盒子碰撞器,选中“GameStart”游戏对象,按步骤单击,“Component”→“Physic”→“Rigidbody”、“Component”→“Physic”→“BoxCollider”,分别为游戏对象添加刚体和盒子碰撞器。并将其勾选为静态的,如图6-17所示。
(10)下面创建游戏中的设置按钮、退出按钮、静音按钮、左右箭头按钮。将模型“GameOption.fbx”拖拉到游戏场景中,调整大小后为其添加刚体和盒子碰撞器组件并将其设置为静态的。五种按钮使用相同的模型,贴图分别是“shezhi.png”、“tuichu.png”、“jingyin.png”、“jixu.png”。创建好后在资源列表中如图6-18所示。
(11)创建选关按钮,将“GameName.fbx”拖到场景中,并重命名为“xuanguan2”,调整大小和位置后将纹理图“bj1.png”拖拉到该游戏对象上,将其勾选为静态的,并添加刚体和盒子碰撞器,如图6-19所示。
(12)按步骤“GameObject”→“Create Other”→“Plane”创建一个平面,重命名为“Guanshu”,并使其成为xuanguan1的子对象。调整大小后将数字图片“num-03”拖拉到该对象上,并将其Shader选为带有透明通道的Transparent/Diffuse。将其勾选为静态的,效果如图6-20所示。
(13)下面创建选关按钮上的分数板,这个游戏对象的主要功能是在“Choose.cs”脚本的左右下在按钮上绘制出获得的星星。将模型“GameStart.fbx”拖到场景中并使其成为 xuanguan1 的子对象,调整大小后将“xingxingban.png”拖拉到该游戏对象上,并将其勾选为静态的。
(14)创建三个 Plane,调整大小并对应放在星星板上的星星位置分别重命名为“Star1”、“Star2”、“Star3”,使其成为fenshuban游戏对象的子对象。为其附加贴图numnull.png并将其着色器设置为带有透明通道的Transparent/Diffuse。效果如图6-21所示。
(15)本场景中共有四个选关按钮“xuanguan1”、“xuanguan2”、“xuanguan3”、“xuanguan4”,制作方法相同,读者可以参照步骤(11)~步骤(14)的方法自行制作和摆放,并将其子对象“Guanshu”的贴图改为相应的关卡数字。
(16)开始层的创建,通过将相似的游戏对象分到同一层,可以在代码中方便地进行统一处理。创建一个层方法时单击属性面板中的“Layer”→“Add Layer…”,创建一个“jinshu”层。并将所有按钮以及游戏名称板都勾选为此层。
(17)创建光源。选择“GameObject”→“Create Other”→“Directional Light”后会自动创建一个定向光源,该种类型的光源类似太阳光,无论设置在什么位置都可以影响到游戏场景中的所有物体,如图6-22所示。调整其参数如图6-23所示。
(18)实现落叶效果,该效果由粒子系统实现,按步骤“GameObject”→“Create Other”→“ParticleSystem”,在组件窗口的粒子系统设计器中可以调整其参数,读者可以自行调整或参看项目中的设置。将粒子的纹理图替换为“shuye.png”,如 图6-24所示。
(19)检查该场景中游戏对象的参数设置,除了主摄像机和粒子系统,其余游戏对象应当全部为静态的,所有的按钮应当加上刚体和相应的碰撞器(MeshCollider或BoxCollider),自行调整灯光和环境光使得光线合适。至此,基本的主菜单场景搭建完毕。
前一小节完成了主场景的搭建,本小节将在搭建好的场景基础上介绍主摄像机相关脚本的开发。实现相应玩家手指滑动屏幕操控摄像机移动和单击主菜单场景中的按钮实现按钮的事件监听的功能,具体步骤如下。
(1)按步骤在Assets 文件夹中单击鼠标右键,选择“Create”→“Folder”新建“script”文件夹。并在该文件夹中单击鼠标右键,在弹出的菜单中选择“Create”→“C# Script”创建脚本,命名为“MyMainMenu.cs”,如图6-25所示。
(2)双击脚本,进入“MonoDevelop”编辑器中,开始脚本的编写。本脚本主要功能为通过3D 拾取技术判断玩家的操控,并根据结果移动摄像机或者实现单击按钮的事件监听、相应玩家的触摸操控。脚本代码如下。
代码位置:见随书光盘中源代码/第06 章目录下的 ColaCola/Assets/script/zhucaidan/MyMainMenu.cs。
1 using UnityEngine;
2 using System.Collections;
3 public class MyMainMenu : MonoBehaviour {
4 public GameObject mycamera; //摄像机游戏对象
5 public GameObject yanwu; //烟雾预制件对象
6 public GameObject huohua; //火花预制件对象
7 public GameObject yinyue; //音乐按钮
8 private float startX = 0; //手指按下X坐标
9 private int fyflag=1; //翻页标志位 0 1 2代表左中右
10 private bool kzflag = false; //是否正在控制标志位
11 private Vector3[] fypos = new Vector3[3]{new Vector3(-5.6f,0,-8),
12 new Vector3(0,0,-8),new Vector3(5.6f,0,-8)}; //摄像机的3个位置
13 void Start(){
14 yinyue.renderer.enabled = !MyStaticClass.yinyue; //初始化当前静音按钮状态
15 }
16 void Update(){
17 foreach (Touch t in Input.touches){ //遍历触摸事件
18 RaycastHit hit; //射线碰撞信息
19 Ray ray = Camera.main.ScreenPointToRay(t.position); //声明射线
20 if (t.phase == TouchPhase.Began) { //若碰撞开始
21 kzflag = true; //开始控制
22 if (Physics.Raycast(ray, out hit)){ //发生射线碰撞
23 if(hit.transform.gameObject.layer==8){ //碰到背景层
24 Instantiate(yanwu,hit.point,hit.transform.rotation); //实例化烟雾
25 }
26 if (hit.transform.gameObject.layer == 17) { //碰到金属层
27 Instantiate(huohua, hit.point, hit.transform.rotation);//实例化火花
28 }
29 if (hit.transform.gameObject.name == "GameStart"){//碰到开始游戏
30 fyflag = 2; //修改翻页标志位
31 }
32 if (hit.transform.gameObject.name == "GameOption"){//碰到设置按钮
33 fyflag = 0; //修改翻页标志位
34 }
35 if (hit.transform.gameObject.name == "zuojiantou"
36 || hit.transform.gameObject.name == "youjiantou") { //碰到左右箭头
37 fyflag = 1; //修改翻页标志位
38 }
39 if (hit.transform.gameObject.name == "jingyin") { //单击静音按钮
40 bool tempsound = MyStaticClass.yinyue; //获取当前声音状态
41 tempsound = !tempsound; //将状态置反
42 MyStaticClass.yinyue = tempsound; //设置声音状态
43 hit.transform.gameObject.renderer.enabled = !tempsound;//设置显示静音反选图片
44 }
45 if(hit.transform.gameObject.name=="GameExit") { //单击退出游戏按钮
46 Application.Quit(); //游戏退出
47 }
48 if(hit.transform.gameObject.name=="xuanguan1"){ //第一关
49 Application.LoadLevel("Level1"); //加载第一关场景
50 }
51 //以下省略一些相似代码,其功能为单击其他的选关按钮加载不同的场景
52 }
53 startX = t.position.x; //记录下按下位置
54 …//以下省略部分代码,下面将详细介绍
55 }}}}
□ 第4行~第12行的主要功能为变量声明,在这里声明了粒子系统、摄像机、音乐按钮游戏对象的引用,以及摄像机移动时的标志位和位置坐标,方便下面的代码调用。
□ 第13行~第15行是对Start方法的重写,在该场景加载时调用,用于根据判断当前是否静音状态来改变静音按钮前的挡板是否渲染,以此改变静音按钮的图案。
□ 第16行~第28行的主要功能为当发生触摸事件时,从触摸点声明一条垂直于主摄像机射出射线的射线,通过射线碰撞判断玩家的动作。当触摸相位为刚按下且玩家单击的是金属层或背景层,则生成相应的粒子系统。
□ 第29行~第38行的主要功能是当用户单击翻页箭头功能按钮、开始游戏按钮或者游戏设置按钮时,根据判断改变翻页标志位,使其准备跳转到相应的界面,下面有代码会根据最终标志位状态对摄像机进行移动实现翻页。
□ 第39行~第55行的主要功能为当玩家单击静音按钮时,将反选图标的显示方式置反,这样就做出了静音按钮和非静音按钮,若玩家单击的是退出按钮则退出游戏,若玩家单击的是选关按钮,就加载相应的关卡。
(3)上面介绍了发生射线碰撞且触摸相位为开始触摸时的代码片段,下面介绍“MyMainMenu.cs”脚本中剩下的代码,包含触摸中相位和触摸结束相位执行的代码,以及最后根据当前翻页标志位的状态进行翻页,代码片段如下。
代码位置:见随书光盘中源代码/第06 章目录下的 ColaCola/Assets/script/zhucaidan/MyMainMenu.cs。
1 if (t.phase == TouchPhase.Moved){ //触摸中相位
2 float dx=t.deltaPosition.x; //当前触摸点X坐标
3 if(Mathf.Abs(this.transform.position.x)>7.2f){ //超出边界
4 return; //返回
5 }
6 this.transform.position = new Vector3(this.transform.position.x-0.01f*dx,0,-8);//屏幕随手指滑动
7 }else
8 if(t.phase==TouchPhase.Ended) { //触摸结束
9 float endX = t.position.x; //记录下手指停下位置
10 if(endX-startX>200) { //向左滑动
11 fyflag = fyflag - 1; //更改翻页标志位
12 if (fyflag < 0) { //当前在就在最左边
13 fyflag = 0; //翻页标志位不变
14 }}
15 else if (endX - startX < -200) { //去右
16 fyflag = fyflag + 1; //更改翻页标志位
17 if (fyflag > 2) { //当前在最右边
18 fyflag = 2; //更改翻页标志位
19 }}
20 kzflag = false; //开始控制标志位为否
21 }}
22 if (!kzflag) { //没有被控制
23 this.transform.position = new Vector3(Mathf.Lerp(this.transform.position.x,fypos[fyflag].x, 3 * Time.deltaTime),this.transform.position.y, this.transform.
24 position.z); //根据标志位差值滑动
25 }}}
□ 第1行~第7行的功能为当发生触摸且触摸相位为手指触摸移动时,记录下手指的位置,并使屏幕随手指滑动而移动,当滑动超出背景范围时停止移动。
□ 第8行~第21行的主要功能是当触摸结束时,很据手指移动的幅度及方式判断怎样使屏幕滑动,并根据判断结果改变翻页标志位。最后将正在控制标志位设置为false。
□ 第22行~第25行的主要功能是当控制结束标志位为真时,根据翻页标志位判断摄像机应当向fypos3数组中具体某个位置移动,并对摄像机执行差值移动,实现翻页的效果。
上一小节已经介绍了主菜单界面中主摄像机的开发过程,本小节将对主菜单界面中选关按钮进行脚本开发的详解,脚本实现了在每个选关按钮上绘制该关卡获得的最大星星数量,并控制星星和主背景的绘制顺序。其具体的讲解内容如下。
创建 C#脚本“Choose.cs”并将其挂载到前面建立好的 11 个选关按钮“xuanguan1”、“xuanguan2”、“xuanguan3”、“xuanguan4”上。该脚本的主要功能是在选关按钮上绘制各关卡已获得的星星数量。脚本代码如下。
代码位置:见随书光盘中源代码/第06 章目录下的 ColaCola/Assets/script/zhucaidan/Choose.cs。
1 using UnityEngine;
2 using System.Collections;
3 public class Choose : MonoBehaviour {
4 public GameObject[] mystar; //星星板游戏对象
5 public GameObject fenshuban; //分数板游戏对象
6 public Material star; //星星材质
7 public Material picnull; //纯透明材质
8 void Start () {
9 int zhu = fenshuban.renderer.material.renderQueue; //获取分数板的渲染次序
10 int level1score = PlayerPrefs.GetInt("level1score"); //获得第一关得到的星星数
11 int level2score = PlayerPrefs.GetInt("level2score"); //获得第二关得到的星星数
12 int level3score = PlayerPrefs.GetInt("level3score"); //获得第三关得到的星星数
13 int level4score = PlayerPrefs.GetInt("level4score"); //获得第四关得到的星星数
14 if(this.gameObject.name=="xuanguan1"){ //第一关分数板
15 for (int i = 0; i < level1score; i++){ //遍历第一关的分数
16 mystar[i].renderer.material = star; //将得分板上的材质改为星星图案
17 mystar[i].renderer.material.renderQueue = zhu + 1; //放在主背景后渲染
18 }
19 }else
20 if (this.gameObject.name == "xuanguan2"){ //第二关分数板
21 for (int i = 0; i < level2score; i++){ //遍历第二关的分数
22 mystar[i].renderer.material = star; //将得分板上的材质改为星星图案
23 mystar[i].renderer.material.renderQueue = zhu + 1; //放在主背景后渲染
24 }
25 }else
26 if (this.gameObject.name == "xuanguan3"){ //第三关分数板
27 for (int i = 0; i < level3score; i++){ //遍历第三关的分数
28 mystar[i].renderer.material = star; //将得分板上的材质改为星星图案
29 mystar[i].renderer.material.renderQueue = zhu + 1; //放在主背景后渲染
30 }
31 }else
32 if (this.gameObject.name == "xuanguan4"){ //第四关分数板
33 for (int i = 0; i < level4score; i++){ //遍历第四关的分数
34 mystar[i].renderer.material = star; //将得分板上的材质改为星星图案
35 mystar[i].renderer.material.renderQueue = zhu + 1; //放在主背景后渲染
36 }}}}
□ 第4行~第7行的主要功能为变量声明,在这里声明了分数板游戏对象和星星板游戏对象数组,以及星星图案的材质和透明材质,以便后面代码调用。
□ 第8行~第13行的主要功能为获取分数板游戏对象的渲染次序,因为某些相距很近的物体在自动进行深度检测时会失败,造成“后面的挡住前面的”效果,所以获取对象的渲染次序后可以代码控制渲染次序,避免这种错误。这里还获得了每一关所得到的星星数量。
□ 第14行~第36行的功能是在游戏场景加载时根据每关获得的星星数量在选关按钮下的星星板上绘制星星,并代码控制其在分数板渲染后渲染。
前面的小节详细介绍了游戏主菜单场景,本小节将介绍游戏主场景的脚本开发,游戏主场景是本案例最重要的场景,也是游戏的关键场景。本游戏共有四个关卡,对应四个不同的场景,其开发方法基本相同,这里以关卡一为例进行详解。
首先进行游戏界面场景的搭建,这里步骤比较繁琐,包括模型的摆放、组件的添加、参数的设置等。通过此小节的开发,读者可以熟练掌握这些知识,同时也会积累一些开发技巧和细节。接下来对游戏场景的搭建进行详细的介绍。
(1)首先新建一个“Level1”场景,具体步骤参考主菜单界面开发的相应步骤,此处不再赘述。需要的图片资源已经放在对应文件夹下,读者可参看本章6.2.2小节的相关内容。
(2)按照步骤“GameObject”→“Create Other”→“Plane”创建四个平面,并分别重命名为“BackGround”、“LeftGround”、“RightGround”、“TopGround”。调整大小和位置使其围成一个前方和底部无盖的盒子的形状。资源列表如图6-26所示。
(3)将步骤(2)创建的四个平面勾选为静态的“Satic”,使其不参加运动。并分别添加刚体和盒子碰撞器,在刚体组件中勾选“IsKinematic”并取消“UseGravity”属性的选择。为“BackGround”游戏对象赋予纹理贴图“beijing.png”。
(4)下面创建放球台子,将模型“TaiZi.fbx”拖拉到场景中,调整大小和位置后赋予其纹理图“taizi1.png”,将其勾选为静态的“Static”。为其添加刚体,并勾选“IsKinematic”属性,取消“UseGravity”,如图6-27所示。
(5)创建放可乐罐台子,其创建方法、组件添加、参数设置方法同步骤(4),使用模型为“TaiZi2.fbx”,纹理图为“Taizi2.png”,调整大小后合理摆放即可。
(6)接下来创建广告板,按照步骤“GameObject”→“Create Other”→“Cube”创建一个立方体,并重命名为“GuanGao”,调整大小后赋予其纹理图“guanggao.png”。为其添加刚体属性和盒子碰撞器组件。在刚体属性中勾选“IsKinematic”和“UseGravity”,如图6-28所示。
(7)下面创建层,对相似的游戏对象划分为同一层可以在代码中方便地进行统一管理。单击属性面板中的“Layer”→“Add Layer…”,创建一个“guanggao”层。并将广告板勾选为此层。用相似方法创建“beijing”层。并将该场景中放罐子台子、四个背景平面勾选为此层。
(8)接下来创建三个球,将模型“muqiu.fbx”拖拉到场景中并重命名为“ball1”、“ball2”、“ball3”,调整大小并分别赋予纹理图“muqiu.png”。调整位置将创建好的三个球放到步骤(4)中创建的放球台子上。效果如图6-29所示。
(9)为每个球添加刚体“Rigidbody”和球体碰撞器“Sphere Collider”组件。添加一个层“ball”并将三个球都选为此层。接下来在Layer下拉选框左边有Tag下拉选框,单击“Tag”→“AddTag…”,添加一个名为“ball”的标签,并将三个球都选为此标签,如图6-30所示。
(10)之后为每个球分别添加声音源使球在特定的情况下可以发出声音。选中球对象,按步骤“Component”→“Audio”→“Audio Source”即创建了一个声音源,将音乐资源“qiuzhuangguan. wav”拖拉到AudioClip属相框中,添加好后如图6-31所示。
(11)下面创建可乐罐,将模型“kele.fbx”拖拉到场景中,调整大小后添加刚体属性和网格碰撞器“Mash Collider”。在本游戏中有三种可乐罐子“普通雪碧”、“爆炸可乐”、“加球雪碧”,分别对应纹理图“languanzi.png”、“hongguanzi.png”、“lvguanzi.png”和层“guanzi”、“guanzi2”、“guanzi3”。
(12)创建好罐子后可以根据关卡设计进行自行摆放,不同种类的罐子只有纹理图和所属层不同。红色的爆炸罐子使用纹理图“hongguanzi.png”,所属层为guanzi2。绿色的加球罐子使用纹理图“lvguanzi.png”,所属层为guanzi3。摆放好后如图6-32所示。
(13)下面创建地板,当球或罐子掉下平台撞击到该游戏对象时会进行计数。按步骤“GameObject”→“Create Other”→“Plane”创建一个平面,并重命名为“Diban”。为其添加刚体组件和盒子碰撞器,在盒子碰撞器中勾选“IsTrigger”使其变为触发器。属性设置如图6-33所示。
(14)接下来创建游戏的控制板。按步骤“GameObject”→“Create Other”→“Cube”创建一个立方体并重命名为“Ban”,调整大小后为其添加纹理图“kongzhiban.png”。并将着色器改为带有透明通道的Transparent/Diffuse。效果如图6-34所示。
(15)下面创建控制板的铁链部分,将模型“liantiao.fbx”拖拉到场景中,调整为灰色并使其成为 Ban 游戏对象的子物体。按步骤“Component”→“Physic”→“Rigidbody”为其添加刚体组件并勾选“IsKinematic”选项。效果如图6-35所示。
(16)在链条的顶端创建一个球体“Sphere”为其添加刚体并勾选“IsKinematic”。选中 Ban游戏对象,按步骤“Component”→“Physic”→“Hinge Joint”为Ban对象添加铰链关节,并把刚才创建好的 Sphere 对象拖拉到铰链关节属性中的 Connected Body 上。关于铰链关节的参数设置如图6-36所示。
(17)接下来制作控制板的按钮部分,在Ban 游戏对象纹理图上的五个按钮位置处创建五个Cube,分别调整大小后从上到下重命名为“kaishi”、“tuichu”、“jingyin”、“chongkaishi”、“zanting”。为五个立方体添加刚体和盒子碰撞器,效果如图6-37所示。
(18)按步骤“Component”→“Physic”→“Fixed Joint”给五个立方体都添加上固定关节,将Connected Body都选为游戏对象“Ban”,给“ jingyin”立方体游戏对象添加纹理图“fanxuan.png”,再将步骤(17)中创建的五个立方体组件列表中的“Mesh Renderer”关闭,使其不进行渲染,如图6-38所示。
(19)下面调整摄像机参数,调整摄像机中“Camera”组件中的“Near”和“Far”参数控制视口范围,“Field of View”参数控制摄像机视口的张角,使游戏中“近大远小”的效果不会过于明显影响到游戏控制。参数调整如图6-39所示。
(20)最后创建灯光,按步骤“GameObject”→“Create Other”→“SpotLight”创建一个区域光,区域光仅在一个锥形区域内有光照,类似有灯罩的台灯。在游戏中使用不同的灯光加以配合会得到非常好的效果。调整参数后,效果如图6-40所示。
前一小节介绍了主场景的搭建过程,本小节将要介绍游戏场景主要游戏对象上挂载的脚本开发过程。首先介绍游戏主场景中的主摄像机相关脚本的开发与设置。具体步骤如下。
(1)新建脚本,命名为“GameRule.cs”并拖曳到“Main Camera”对象上,该脚本用于实现游戏的规则,判定胜负。当球发射完毕而罐子还有剩余的时候游戏失败,当罐子全部被砸落时游戏胜利,并根据游戏的胜利情况生成相应的分数板。脚本代码如下。
代码位置:见随书光盘中源代码/第06章目录下的ColaCola/Assets/script/GameRule.cs。
1 using UnityEngine;
2 using System.Collections;
3 public class GameRule : MonoBehaviour {
4 public GameObject GuanZi; //罐子游戏对象
5 public GameObject fenshuban; //分数板游戏对象
6 private GameObject scoreBoard; //实例化后分设板游戏对象
7 public GameObject fenshuban2; //失败分数板游戏对象
8 public Camera mycamera; //主摄像机
9 public AudioSource win; //游戏胜利音效
10 private bool jfflag = true; //加分标志位只加一次
11 private float temptime; //加分时间
12 public GUIStyle myStyle; //GUI风格
13 public Texture2D jia1000; //加1000分图片
14 void Update(){
15 if (Input.GetKeyUp(KeyCode.Escape)) { //安卓手机返回按键监听
16 Application.LoadLevel("Zhujiemian"); //返回游戏主界面
17 }
18 int gzshengyu=0; //声明罐子剩余数量变量
19 foreach (Transform childs in GuanZi.transform){ //罐子子对象
20 gzshengyu++; //统计罐子数量
21 }
22 GameObject[] ball=GameObject.FindGameObjectsWithTag("ball");//所有标签为ball的物体数组
23 if (gzshengyu == 0){ //剩余罐子为0
24 MyStaticClass.shengfu=1; //游戏胜利
25 StartCoroutine(Waitforsecond1(2.0f,1.0f));//启动协同程序等2秒游戏结束,1秒生成分数板
26 if (scoreBoard != null) { //若已经生产分数板
27 scoreBoard.transform.position = new Vector3(scoreBoard.transform.position.x,
28 Mathf.Lerp(scoreBoard.transform.position.y, 4f, Time.deltaTime),scoreBoard.transform.position.z);
29 }}
30 else if (ball.Length == 0){ //罐子不为0,球射完
31 MyStaticClass.shengfu = 2; //游戏失败
32 StartCoroutine(Waitforsecond2(3.0f)); //启动协同程序两秒后游戏结束
33 if (scoreBoard != null){ //若没有生产分数板
34 scoreBoard.transform.position = new Vector3(scoreBoard.transform.position.x, Mathf.Lerp(scoreBoard.transform.position.y, 4f, Time.deltaTime), scoreBoard.
35 transform.position.z);
36 }}
}IEnumerator Waitforsecond1(float waitTime1,float waitTime2){ //协同程序137
38 yield return new WaitForSeconds(waitTime1); //延迟waitTime1秒
39 MyStaticClass.gameoverflag = true; //游戏结束
40 yield return new WaitForSeconds(waitTime2); //等待加分(剩余球)
41 if (scoreBoard == null){ //若没有生产分数板
42 if (MyStaticClass.yinyue){ //不是静音状态
43 win.Play(); //播放胜利音效
44 }
45 scoreBoard = Instantiate(fenshuban, new Vector3(0.04f, 16f, -10f),
46 this.transform.rotation) as GameObject; //实例化分数板并取得其对象引用
47 }}
48 IEnumerator Waitforsecond2(float waitTime1){ //协同程序2
49 yield return new WaitForSeconds(waitTime1); //延迟waitTime1秒
50 MyStaticClass.gameoverflag = true; //游戏结束
51 if (scoreBoard == null) { //若没有生产分数板
52 scoreBoard=Instantiate(fenshuban2,newVector3(0.04f,16f,-10f),
53 this.transform.rotation) as GameObject; //实例化分数板并取得其对象引用
54 }}}
□ 第4行~第13行的主要功能为变量的声明。在这里声明了分数板游戏对象、主摄像机、胜利音效、加分标志位、加分计时器和图片等对象,方便下面的代码使用。
□ 第14行~第17行的主要功能是对手机硬件上的返回按键进行监听,当玩家在游戏关卡中按下返回按键时,会跳转到主菜单场景。
□ 第18行~第29行首先遍历GuanZi游戏对象的子对象,统计出当前剩余罐子数量,并根据查找标签得到当前剩余球的数量。若是可乐罐已经全部销毁,更改标志位shengfu为游戏胜利,启动协同程序,生成分数板并播放音效。
□ 第30行~第36行的主要功能为若是罐子还有剩余但球已经全部使用完并被销毁,则更改游戏胜负标志位shengfu为游戏失败,启动协同程序生成失败分数板,并将失败分数板差值滑动到屏幕中指定位置。
□ 第37行~第54行是两个协同程序,协同程序是在主程序运行的同时开启另一条逻辑处理来协同当前程序执行。换句话说,协同程序就是开启一个进程。这里被用于等待一段时间后改变游戏状态标志位,并生成分数板赋予对象“ScoreBoard”,便于主程序使用。
(2)下面是该脚本中对OnGUI方法的重写。OnGUI方法每帧调用若干次,掌管游戏中的绘制,如绘制按钮、图片等。在该方法中实现了当游戏结束后,根据罐子位置在其上方绘制罐子的加分动画。脚本片段代码如下。
代码位置:见随书光盘中源代码/第06章目录下的ColaCola/Assets/script/GameRule.cs。
1 void OnGUI()//绘制剩余球加分{
2 if (MyStaticClass.gameoverflag){ //若是游戏结束
3 GameObject[] ball = GameObject.FindGameObjectsWithTag("ball");//所有标签为ball的物体数组
4 Vector3 [] ballpos=new Vector3[ball.Length]; //声明用于保存球位置的数组
5 for (int i = 0; i < ball.Length; i++){ //遍历每个球
6 ballpos[i] = mycamera.WorldToScreenPoint(ball[i].transform.position);//取出当前球的位置
7 temptime += Time.deltaTime;//计时
8 if (jfflag){ //若可以加分
9 MyStaticClass.shengqiu++; //记录剩余球
10 MyStaticClass.setzongfen(MyStaticClass.getzongfen() + 1000); //加分
11 }
12 if (temptime <= 3){ //加分时间小于3秒
13 GUI.Label(new Rect(ballpos[i].x, Screen.height * 4 / 5,
14 40 / temptime, 10 / temptime), jia1000, myStyle);//绘制加分图片,大小随时间变小
15 }}
16 jfflag = false; //只加分一次,关闭加分标志位
17 }}
□ 第1行~第4行的主要功能是判断当前是否游戏结束,若是已经结束,就通过标签找到所有剩余的球,并声明一个三维坐标数组用于储存球的位置。
□ 第5行~第11行开始遍历剩余球的游戏对象数组,每找到一个剩余的球就记录下位置,若是第一次加分,就给总分加1000分,并最后统计出台子上剩余球的数量。
□ 第12行~第17行的主要功能为绘制加分动画,根据加分时间改变jia1000.png的大小,使其达到加分图案由大缓缓变小的视觉效果,形成加分动画。最后关闭加分标志位,防止重复加分。
(3)在本游戏的某些关卡具有特殊的罐子,如红色的爆炸罐子、绿色的加球罐子。若是在这些关卡开始的时候显示针对本关特色的提示板,会大大加强游戏体验。下面介绍这个提示板的实现方法。新建脚本“TiShi.cs”并挂载到主摄像机上。脚本代码如下。
代码位置:见随书光盘中源代码/第06章目录下的ColaCola/Assets/script/TiShi.cs。
1 using UnityEngine;
2 using System.Collections;
3 public class TiShi : MonoBehaviour {
4 public GameObject level2Tishi; //第二关提示板
5 public GameObject level3Tishi; //第三关提示板
6 public GameObject ball1; //球1对象
7 public GameObject ball2; //球2对象
8 public GameObject ball3; //球3对象
9 public GameObject huohua; //火花粒子
10 private bool ssflag=false; //上升提示板标志位
11 private bool xjflag = true; //下降提示板标志位
12 private GameObject tiShi; //提示板
13 void Start () {
14 if (Application.loadedLevelName=="Level1"){ //当前是第一关
15 MyStaticClass.gamestart = true; //游戏直接开始,没有提示板
16 }else
17 if (Application.loadedLevelName == "Level2"){ //第二关
18 MyStaticClass.gamestart = false; //游戏暂时不开始
19 Ball b1 = (Ball)ball1.GetComponent("Ball"); //显示提示板的时候让球1不受控制
20 b1.enabled = false; //关闭Ball脚本
21 Ball b2 = (Ball)ball2.GetComponent("Ball"); //显示提示板的时候让球2不受控制
22 b2.enabled = false; //关闭Ball脚本
23 Ball b3 = (Ball)ball3.GetComponent("Ball"); //显示提示板的时候让球3不受控制
24 b3.enabled = false; //关闭Ball脚本
25 tiShi = Instantiate(level2Tishi, new Vector3(0.04f, 16f, -10f),
26 this.transform.rotation) as GameObject; //实例化分数板并取得其对象引用
27 } else
28 if (Application.loadedLevelName == "Level3"){ //第三关
29 MyStaticClass.gamestart = false; //游戏暂时不开始
30 Ball b1 = (Ball)ball1.GetComponent("Ball"); //显示提示板的时候让球不受控制
31 b1.enabled = false; //关闭Ball脚本
32 Ball b2 = (Ball)ball2.GetComponent("Ball"); //显示提示板的时候让球不受控制
33 b2.enabled = false; //关闭Ball脚本
34 Ball b3 = (Ball)ball3.GetComponent("Ball"); //显示提示板的时候让球不受控制
35 b3.enabled = false; //关闭Ball脚本
36 tiShi = Instantiate(level3Tishi, new Vector3(0.46f, 16f, -10f),
37 this.transform.rotation) as GameObject; //实例化分数板并取得其对象引用
38 }}
39 void Update(){
40 ...//以下省略对UpDate方法的重写,下面将详细介绍
41 }}
□ 第1行~第12行为变量声明,在这里声明了两种特殊罐子对应的提示板预制件对象、三个木球游戏对象、火花粒子系统对象、上升下降标志位等变量、生成后的提示板对象等,方便下面代码使用或更改其状态。
□ 第13行~第16行是对Start方法的重写,该方法在本脚本第一次加载时调用。在这里的主要功能是取出当前的关卡名,判断当前是否为游戏的第一关。若是第一关,没有提示板,游戏直接开始。否则先生成游戏提示板,之后再开始。
□ 第17行~第37行的主要功能是根据当前运行的场景名判断,若当前是第二关或者第三关,首先关闭球上的脚本“Ball.cs”锁定球,使得球暂时不受玩家控制。并在指定位置生成对应的提示板,并通过差值移动将提示板移动到屏幕中央。
(4)下面介绍“Tishi.cs”脚本中对 Update 方法的重写,该方法在游戏的每帧都调用一次,在本脚本中的具体功能为,判断用户的触摸事件改变提示板上升或下降标志位,之后根据标志位对提示板进行移动,在显示提示板的时间里使球不能发射。代码片段如下。
代码位置:见随书光盘中源代码/第06章目录下的ColaCola/Assets/script/TiShi.cs。
1 void Update () {
2 if (tiShi != null) { //已经生成提示板
3 if (xjflag){ //下降标志位
4 tiShi.transform.position = new Vector3(tiShi.transform.position.x,
5 Mathf.Lerp(tiShi.transform.position.y, 5f, Time.deltaTime), tiShi.transform.position.z);//下移提示板动画
6 if (tiShi.transform.position.y < 5.5f){ //到达最低位置
7 xjflag = false; //停止下降
8 }
9 return; //跳过下面的代码
10 }
11 if (ssflag) { //上升
12 tiShi.transform.position = new Vector3(tiShi.transform.position.x,Mathf.Lerp(tiShi.transform.position.y, 17f, Time.deltaTime), tiShi.transform.position.z);//上移提示板动画
13
14 if (tiShi.transform.position.y > 16.5f){ //到达最高位置
15 MyStaticClass.gamestart = true; //游戏开始
16 Ball b1 = (Ball)ball1.GetComponent("Ball"); //让球受玩家控制
17 b1.enabled = true; //开启Ball脚本
18 Ball b2 = (Ball)ball2.GetComponent("Ball"); //让球受玩家控制
19 b2.enabled = true; //开启Ball脚本
20 Ball b3 = (Ball)ball3.GetComponent("Ball"); //让球受玩家控制
21 b3.enabled = true; //开启Ball脚本
22 ssflag = false; //上升标志位改为false
23 Destroy(tiShi); //删除提示板对象
24 }
25 return;
26 }
27 foreach (Touch t in Input.touches){ //遍历触控事件
28 RaycastHit hit; //声明射线碰撞
29 Ray ray = Camera.main.ScreenPointToRay(t.position); //从摄像机位置发射一条射线
30 if (t.phase == TouchPhase.Began){ //触碰开始
31 if (Physics.Raycast(ray, out hit)){ //发生射线碰撞
32 if (hit.transform.gameObject.name == "jixu"){ //单击到继续按钮
33 ssflag = true; //上升标志位设为true
34 }
35 if (hit.transform.gameObject.name == "kongzhiban"){ //点到控制板
36 hit.transform.gameObject.rigidbody.AddForce(new Vector3(0, 0, 3),ForceMode.Impulse); //加力
37 Instantiate(huohua, hit.point, hit.transform.rotation); //显示火花
38 }
39 if (hit.transform.gameObject.name == "kele"){ //点到可乐罐子
40 hit.transform.gameObject.rigidbody.AddForce(new Vector3(0, 0, 5),ForceMode.Impulse); //加力
41 Instantiate(huohua, hit.point, hit.transform.rotation); //显示火花
42 }}}}}
□ 第1行~第10行的主要功能为当提示板已经生成后,若当前下降标志位为真就开始使提示板差值下降,当高度小于5.5时停止下降,移动完毕后跳过下面代码。
□ 第11行~第26行的主要功能是当上升标志位为真时,匀速上升提示板,当摄像机看不到提示板时,开启Ball.cs脚本使球可以进行发射,将游戏开始标志位改为“true”,并销毁提示板。
□ 第27行~第42行的主要功能为辨别触摸事件。当发生触控时,从触控点发射一条射线,若是碰到继续按钮就把上升标志位改为真,若是碰到板子或者罐子就在碰撞点加一个瞬间力,并实例化火花粒子系统。
上一小节介绍了游戏主场景中主摄像机的开发过程,本小节将详细介绍球类的开发方法和其对应的脚本开发,具体实现了玩家触摸滑动屏幕进行发球以及球在飞行途中碰到特殊物体的特殊处理办法,如广告版、爆炸球等。具体步骤如下。
(1)新建脚本“Ball.cs”并挂载到每个球上,该脚本的主要功能为判断玩家手指的滑动方式,在其上添加相应的力,实现发球或调整球位置。同时当球在落下时碰到位于屏幕以下的地板触发器,删除球以节省资源。脚本代码如下。
代码位置:见随书光盘中源代码/第06章目录下的ColaCola/Assets/script/Ball.cs。
1 using UnityEngine;
2 using System.Collections;
3 public class Ball : MonoBehaviour {
4 public GameObject ball; //球对象
5 bool fsflag=false; //发射标志位
6 public LayerMask mask=-1; //层数
7 public Camera mycamera; //主摄像机
8 public Texture2D jia1000; //加1000分图片
9 private float startY = 0; //触控开始时Y坐标
10 private float startX = 0; //触控开始时X坐标
11 private float zsp=10f; //给球施加在Z轴的力
12 private float dx=0; //手指帧X坐标位移
13 private float dy=0; //手指帧Y坐标位移
14 private bool scflag = false; //删除标志位
15 private float sctime=0; //删除时间
16 void Start () {
17 scflag = false; //删除该球标志位置为false
18 }
19 void FixedUpdate () {
20 if (scflag){ //碰到地板触发器
21 sctime += Time.deltaTime; //计时
22 if (sctime > 3){ //3秒后删除
23 Destroy(this.gameObject); //删除球
24 }
25 return; //返回
26 }
27 if(this.transform.position.z>-11&&this.transform.position.z<-3){//球在不可控范围内
28 this.rigidbody.AddForce(new Vector3(0,0,3*zsp),ForceMode.Force);//施加一个瞬间力
29 return;
30 }
31 foreach(Touch t in Input.touches) { //遍历触摸事件
32 RaycastHit hit; //射线碰撞信息
33 if(t.phase==TouchPhase.Began) { //若碰撞开始
34 fsflag=false; //关闭发射标志位
35 Ray ray=Camera.main.ScreenPointToRay(t.position); //定义射线
36 if(Physics.Raycast(ray,out hit,Mathf.Infinity,mask.value)) {//发生射线碰撞
37 if(hit.transform.root.transform==this.transform //碰到该对象
38 &&transform.position.y<0.5) { //球在可触摸位置
39 fsflag=true; //可以发射
40 startX = t.position.x; //触碰起始X位置
41 startY = t.position.y; //触碰起始Y位置
42 }}}
43 else if(t.phase==TouchPhase.Moved&&fsflag) { //滑动过程中
44 rigidbody.WakeUp(); //唤醒刚体
45 dy=t.deltaPosition.y; //记录手指Y帧位移
46 dx=t.deltaPosition.x; //记录手指Y帧位移
47 float moveX=t.position.x; //记录手指X坐标位置
48 float moveY=t.position.y; //记录手指Y坐标位置
49 if(moveY-startY<50) { //手指水平滑动
50 this.rigidbody.AddForce(new Vector3(dx,0,0),ForceMode.Force);//使球左右滑动
51 }else
52 if(moveY-startY>=50) { //手指向上滑动
53 this.rigidbody.AddForce(new Vector3(3*dx+(startX-Screen.width/2)/16,
54 dy * 2, 8 * zsp), ForceMode.Force); //给球添加一个力
55 }}}}
56 void OnTriggerEnter(Collider target){ //碰到触发器
57 if (target.gameObject.layer == 11) { //球撞到地板
58 scflag = true; //删除标志位改为true
59 }}}
□ 第1行~第15行为变量声明,在本脚本中使用到较多变量,在这里进行声明并初始化。有球游戏对象、摄像机、发球标志位、加分图片、删除标志位、计时器以及各类位置坐标用于记录手指的位置,以便下面代码调用。
□ 第16行~第18行是对Start方法的重写,在本场景加载时调用。在这里的主要功能是初始化删除标志位为“false”。当该标志位为“true”时会对本球进行删除,所以在游戏开始时对其进行初始化,避免错误删除。
□ 第19行~第30行的主要功能是判断删除标志位是否为真,若满足,就开始计时,并在3秒后删除该球。若是球在不可控制范围内,就添加一个向前的力,这样的射击效果会相较于让球按照抛物线飞行可控性更高,降低了游戏的难度。
□ 第31行~第42行的主要功能为当发生触摸事件时,从主摄像机声明射线判断触摸到哪个游戏对象,若是碰到球并且触摸相位为开始触摸时,将球设置为可以发射状态并记录下手指位置。
□ 第43行~第55行是当触摸相位为触摸中时,记录下手指位置和手指滑动帧位移,并用这些变量计算出一个修正量,最后给球施加一个带有修正量的力,实现发球或者水平拖动球。该修正量的作用是消除由于视锥口产生的“近大远小”的视觉误差影响射击的问题。
□ 第56行~第59行是对“OnTriggerEnter”方法的重写。当该游戏对象碰到触发器时调用,若是碰到地板触发器,说明球已经掉出屏幕,所以将删除标志位改为true。在Update中3秒后会对该对象进行删除,避免后台对该球做多余的运动计算。
(2)下面开发球的碰撞脚本,新建脚本“LiZi.cs”并将其挂载到每个球游戏对象上,该脚本处理球的碰撞事件,如碰撞到爆炸罐子发生的爆炸事件、碰到加球罐子会增加一个球、碰到其他物体播放粒子系统同时播放音效等。脚本代码如下。
代码位置:见随书光盘中源代码/第06章目录下的ColaCola/Assets/script/LiZi.cs。
1 using UnityEngine;
2 using System.Collections;
3 public class LiZi : MonoBehaviour {
4 public GameObject yanwu; //烟雾粒子系统
5 public GameObject huohua; //火花粒子系统
6 public GameObject baozha1; //爆炸特效1
7 public GameObject baozha2; //爆炸特效2
8 private float mytime; //计时器
9 public GameObject ball; //球游戏对象
10 public GameObject baozhaguanzi; //炸碎的罐子预制件
11 public AudioSource qiuzhuangguan; //球撞罐子音效
12 void OnCollisionEnter(Collision target){
13 if (target.gameObject.layer == 12){ //爆炸罐子
14 Vector3 explosionPos = target.gameObject.transform.position; //爆炸点
15 Destroy(target.gameObject); //销毁爆炸罐子
16 Instantiate(baozhaguanzi,target.gameObject.transform.
17 position,target.gameObject.transform.rotation);//在爆炸罐子的地方实例化炸碎的罐子
18 float radius =4; //爆炸半径
19 float power = 5; //爆炸力
20 Collider[] colliders = Physics.OverlapSphere (explosionPos, radius);//爆炸相交球
21 foreach (Collider hit in colliders){ //遍历爆炸相交球碰到的物体
22 if(hit.rigidbody){ //碰到的是刚体
23 hit.rigidbody.AddExplosionForce(power, explosionPos, radius, 3.0f, ForceMode.Impulse); //爆炸力
24 }}
25 Instantiate(baozha1, this.transform.position, this.transform.rotation);//实例化爆炸火花1
26 Instantiate(baozha2, this.transform.position, this.transform.rotation);//实例化爆炸火花2
27 }
28 if (target.gameObject.layer == 16){ //加球罐子
29 Instantiate(ball,new Vector3(-4.6f,-2.7f,-14.8f),this.transform.rotation);//添加一个球
30 target.gameObject.layer = 9; //将加球管子变成普通罐子
31 }
32 if(target.gameObject.layer==10) { //广告板
33 target.rigidbody.isKinematic = false; //取消对广告板的锁定
34 instantiate(huohua,this.transform.position,this.transform.rotation);//火花粒子
35 }
36 if(target.gameObject.layer==8){ //背景
37 Instantiate(yanwu,this.transform.position,this.transform.rotation);//实例化烟雾粒子系统
38 }else
39 if(target.gameObject.layer==9) { //罐子
40 if (MyStaticClass.yinyue){ //如果不是静音状态
41 qiuzhuangguan.Play(); //播放球撞罐子音效
42 }
43 instantiate(huohua,this.transform.position,this.transform.rotation);//火花粒子系统
44 }}}
□ 第1行~第11行是变量声明,在这里声明了许多变量如各种爆炸火花的粒子系统、炸碎后罐子的预制件、计时器、游戏中的碰撞音效等,供下面代码使用。
□ 第12行~第27行的主要功能为当球碰撞到爆炸罐子时,先销毁爆炸罐子,并在该位置上实例化炸碎罐子的预制件,之后在碰撞点生成一个相交球,对相交球接触到的刚体都施加一个来自爆炸点的力,以此来实现炸飞的效果。
□ 第28行~第31行为球撞到加球的罐子,在放球台子的最左边实例化一个球,球会在重力的作用下慢慢滚入屏幕,最后将撞到的加球罐子的层“guanzi3”改为普通罐子所处的层“guanzi”,避免下次再次碰到该球时进行重复加球。
□ 第32行~第35行的主要功能为当球碰到广告板的时候,激活广告板的刚体属性中的isKinematic 组件,解除其锁定状态使其可以在重力的作用下自由下降,实现将广告板撞落的效果。并在碰撞点实例化火花粒子系统。
□ 第36行~第44行的主要功能是当球碰到背景或罐子时,实例化烟雾粒子系统或火花粒子系统,并从静态类中取出“yinyue”标志位判断当前是否为静音状态,判断是否播放音效。
在上一小节中介绍了球的脚本开发,本小节将详细介绍罐子上所挂载脚本“GuanZi.cs”的开发,该脚本实现了当罐子落下平台后播放加分动画的功能。具体步骤如下。
新建脚本“GuanZi.cs”并挂载到每个罐子游戏对象上。该脚本主要用于当罐子落下平台时播放加分动画。脚本代码如下。
代码位置:见随书光盘中源代码/第06章目录下的ColaCola/Assets/script/GuanZi.cs。
1 using UnityEngine;
2 using System.Collections;
3 public class GuanZi : MonoBehaviour {
4 rivate int flag=0; //播放加分标志位
5 public Texture2D jia100; //加100图片
6 public GUIStyle myStyle; //GUI风格
7 public Camera mycamera; //主摄像机
8 Vector3 wtos; //球掉落的位置
9 private float temptime; //显示加分的时间
10 void Update () {
11 if(flag==1){ //开始加分
12 temptime += Time.deltaTime; //开始计时
13 }}
14 void OnGUI(){
15 if(flag==1&&temptime<2){ //加分且播放时间小于2秒
16 GUI.Label(new Rect(wtos.x,Screen.height/2,
17 30/temptime,10/temptime),jia100,myStyle); //显示加分2秒
18 }else
19 if(temptime>=2){ //两秒后
20 flag = 0; //重置标志位
21 temptime = 0.1f; //重置时间
22 Destroy(this.gameObject); //销毁罐子
23 }}
24 void OnTriggerEnter(Collider target){ //碰到触发器
25 if(target.gameObject.layer==11){ //罐子撞到地板
26 MyStaticClass.setzongfen(MyStaticClass.getzongfen() + 100);//加分
27 wtos = mycamera.WorldToScreenPoint(this.transform.position);//罐子掉下的位置
28 flag=1; //播放加分
29 }}}
□ 第1行~第9行的主要功能为变量声明,在这里声明了加分标志位、加分图片、摄像机游戏对象、球位置坐标等,以便下面的代码使用。
□ 第10行~第13行是对Update方法的重写,该方法每帧调用一次,这里的作用是当加分标志位为1时,使计时器temptime开始计时。
□ 第14行~第23行是对OnGUI方法的重写,该方法用于在屏幕上绘制图片,每帧调用若干次。这里的作用是当加分标志位为 1 时,开始绘制加 100 分图片,同时使该图片在播放的过程中进行缩放。2秒后停止绘制并删除罐子游戏对象。
□ 第24行~第29行是对OnTriggerEnter方法的重写,当游戏对象撞击到触发器时调用。当罐子碰撞到地板层(触发器)时加分,并记录下掉落位置。将加分标志位改为可加分状态,后面将根据该标志位进行加分。
上一个小节介绍了游戏中罐子的开发,下面将详细介绍游戏场景中控制板的脚本开发,控制板处于游戏的左上角,上面有一系列功能按钮如暂停按钮、重新开始按钮、静音按钮、继续按钮、退出游戏按钮,下面将介绍这些功能的实现。具体步骤如下。
(1)新建脚本“GameControlPanel.cs”并挂载在游戏控制板游戏对象上。该脚本用于实现手指滑动控制控制板的上升和下降,以及单击控制板上各个按钮所实现的功能。脚本代码如下。
代码位置:见随书光盘中源代码/第06章目录下的ColaCola/Assets/script/GameControlPanel.cs。
1 using UnityEngine;
2 using System.Collections;
3 public class GameControlPanel : MonoBehaviour {
4 private bool gnflag=false; //false简要功能 true全部功能
5 private bool cmflag = false; //触摸到控制板标志位
6 private int donghua =0; //1上移 2下移
7 public LayerMask mask; //层数
8 private float startY = 0; //手指按下位置
9 public GameObject joint; //控制板连接关节点
10 public GameObject huohua; //粒子系统
11 public GameObject yinyue; //音效按钮
12 public bool ksflag = true; //游戏开始标志位
13 void Start () {
14 MyStaticClass.gameoverflag = false; //初始化游戏结束标志位
15 MyStaticClass.shengqiu = 0; //初始化剩余球数
16 yinyue.renderer.enabled = !MyStaticClass.yinyue; //根据是否静音确定静音按钮的图案
17 }
18 void FixedUpdate(){
19 if (MyStaticClass.gamestart&&ksflag){ //游戏开始
20 this.transform.position = new Vector3(this.transform.position.x,//下降控制板动画
21 Mathf.Lerp(this.transform.position.y, 8f, Time.deltaTime), this.transform.position.z);
22 joint.transform.position = new Vector3(joint.transform.position.x,//下降关节点动画
23 Mathf.Lerp(joint.transform.position.y, 13.66f, Time.deltaTime), joint.transform.position.z);
24 if(this.transform.position.y<8.1f){ //下降到最低点
25 ksflag = false; //将开始标志位置为false
26 }}
27 if (MyStaticClass.gameoverflag){ //若是游戏结束
28 this.transform.position = new Vector3(this.transform.position.x,//上升控制板动画
29 Mathf.Lerp(this.transform.position.y, 11.9f, Time.deltaTime), this.transform.position.z);
30 joint.transform.position = new Vector3(joint.transform.position.x,//上升关节点动画
31 Mathf.Lerp(joint.transform.position.y, 17.5f, Time.deltaTime), joint.transform.position.z);
32 return;//返回
33 }
34 ...//这里省略了一些代码,下面将详细介绍
35 }}
□ 第1行~第12行的主要功能是变量声明,在这里声明了功能标志位、触摸标志位、粒子系统等游戏变量,以便下面调用。
□ 第13行~第17行是对Start方法的重写,在本场景加载时调用。在这里的功能是初始化游戏结束标志位、剩余球数并且根据静态类中静音标志位的状态决定静音按钮的图案。
□ 第18行~第35行是对“FixedUpdate”方法的重写,该方法每过固定时间调用一次。在这里的主要功能是当游戏开始标志位为真时下降控制板和控制板关节点,对应当游戏结束标志位为真时上升控制板和关节点。
(2)下面讲解实现控制板上升或者下降动画的代码片段,主要思路是根据之前修改过的标志位对控制板对象进行差值滑动。实现代码如下。
代码位置:见随书光盘中源代码/第06 章目录下的 ColaCola/Assets/script/GameControl Panel.cs。
1 if (donghua == 1){ //上移动画
2 if (this.transform.position.y > 8.0f){ //到达指定位置
3 this.transform.position = new Vector3(this.transform.position.x, 8.07f, this.transform.position.z);//改变阀值
4 donghua = 0; //停止播放动画
5 GameObject[] ball = GameObject.FindGameObjectsWithTag("ball");//所有标签为ball的物体数组
6 for (int i = 0; i < ball.Length; i++){ //遍历球对象数组
7 Ball b = (Ball)ball[i].GetComponent("Ball"); //找到球上的Ball.cs脚本组件
8 b.enabled = true; //开启脚本,使球受控制
9 }
10 return; //返回
11 }
12 this.transform.position = new Vector3(this.transform.position.x,//上升控制板动画
13 Mathf.Lerp(this.transform.position.y, 8.07f, Time.deltaTime), this.transform.position.z);
14 joint.transform.position = new Vector3(joint.transform.position.x,//上升关节点动画
15 Mathf.Lerp(joint.transform.position.y,13.65051f,Time.deltaTime),joint.transform.position.z);
16 gnflag = false; //改为简易功能标志位
17 }
18 else if (donghua == 2){ //下移动画
19 GameObject[] ball = GameObject.FindGameObjectsWithTag("ball");//所有标签为ball的物体数组
20 for (int i = 0; i < ball.Length; i++){ //遍历球对象数组
21 Ball b = (Ball)ball[i].GetComponent("Ball"); //找到球上的Ball.cs脚本组件
22 b.enabled = false; //关闭脚本,使球不受控制
23 }
24 if (this.transform.position.y < 4.6f){ //到达指定位置
25 this.transform.position = new Vector3(this.transform.position.x,4.58f,this.transform.position.z);//改变阀值
26 donghua = 0; //停止播放动画
27 return; //返回
28 }
29 this.transform.position=new Vector3(this.transform.position.x,//下降控制板动画
30 Mathf.Lerp(this.transform.position.y, 4.58f, Time.deltaTime), this.transform.position.z);
31 joint.transform.position = new Vector3(joint.transform.position.x,//下降关节点动画
32 Mathf.Lerp(joint.transform.position.y, 10.16f, Time.deltaTime), joint.transform.position.z);
33 gnflag = true; //当前为全部功能
34 }
□ 第1行~第17行的主要功能为实现了控制板上移动画,使控制板和控制板上挂载的铰链关节点同步差值向上滑动,到达指定地点后遍历所有球对象,并将上面挂载的“Ball”脚本逐个开启,使球受玩家控制。
□ 第18行~第34行的主要功能为实现控制板下移动画,使控制板和控制板上挂载的铰链关节点同步差值向下滑动,到达指定地点后遍历所有球对象,并将上面挂载的“Ball”脚本逐个关闭,使球不受玩家控制。
(3)下面介绍如何实现通过判断玩家的触摸行为,使控制板做相应的运动,同时包含控制板上暂停按钮、重新开始按钮、继续按钮、退出按钮、静音按钮的按键监听的代码,代码片段如下。
代码位置:见随书光盘中源代码/第06 章目录下的 ColaCola/Assets/script/GameControl Panel.cs。
1 foreach (Touch t in Input.touches) { //遍历触控事件
2 RaycastHit hit; //声明射线碰撞
3 if (t.phase == TouchPhase.Began) { //触碰开始
4 cmflag = false; //初始化触摸到控制板标志位
5 Ray ray = Camera.main.ScreenPointToRay(t.position); //发射一条射线
6 if (Physics.Raycast(ray, out hit, Mathf.Infinity, mask.value)){//射线碰撞
7 if (hit.transform.root.transform == this.transform){ //碰到该对象
8 cmflag = true; //触摸标志位为true
9 startY = t.position.y; //记录触摸起始位置
10 }}}
11 else if (t.phase == TouchPhase.Moved&&cmflag){ //手指移动中
12 float moveY = t.position.y; //记录当前移动位置
13 if (moveY - startY > 10 && gnflag){ //向上滑且当前是全部功能
14 donghua = 1; //动画上移
15 }
16 else if (moveY - startY < -10 && !gnflag){ //向下滑且当前是简要功能
17 donghua = 2; //动画下移
18 }}
19 else if (t.phase == TouchPhase.Ended&&cmflag){ //触摸事件结束
20 Ray ray = Camera.main.ScreenPointToRay(t.position); //声明射线
21 if (Physics.Raycast(ray, out hit, Mathf.Infinity, mask.value)){ //射线碰撞
22 if (hit.transform.gameObject.name == "Ban"){ //触摸到板
23 this.rigidbody.AddForce(new Vector3(0, 0, 3), ForceMode.Impulse);//加一个瞬间力
24 Instantiate(huohua, hit.point, hit.transform.rotation); //显示火花
25 }
26 if (hit.transform.gameObject.name == "zanting"){ //暂停按钮
27 if(!gnflag){ //功能为简易版
28 donghua = 2; //开始下降动画
29 }}
30 if (hit.transform.gameObject.name == "chongkaishi") { //重新开始按钮
31 MyStaticClass.setzongfen(0); //重置分数为0
32 MyStaticClass.gameoverflag = false; //初始化游戏结束标志位为否
33 Application.LoadLevel(Application.loadedLevelName);//重新加载当前场景
34 }
35 if (hit.transform.gameObject.name == "kaishi"){ //开始按钮
36 donghua = 1; //转换为简要按钮模式
37 }
38 if (hit.transform.gameObject.name == "jingyin"){ //静音按钮
39 bool tempflag = MyStaticClass.yinyue; //获取当前是否静音
40 tempflag = !tempflag; //置反
41 MyStaticClass.yinyue = tempflag; //重置标志位
42 hit.transform.gameObject.renderer.enabled = !tempflag;//更改静音按钮图案
43 }
44 if (hit.transform.gameObject.name == "tuichu"){ //退出按钮
45 Application.LoadLevel("Zhujiemian"); //加载主菜单界面
46 }}}}
□ 第1行~第10行的主要功能是当手指放下时,声明一条从摄像机发出的垂直于摄像机的射线。若是该射线碰到控制板就将触摸标志位 cmflag 改为 true,并记录下手指当前的位置startY。
□ 第11行~第18行是触摸相位为手指移动中,记录手指的位移,若是手指为向下滑动并且当前控制板的功能为简易功能,就改变动画标志位donghua为2,播放下移动画。若是手指向上滑动,则改变标志位donghua为1,播放上移动画。
□ 第19行~第46行为触摸结束时执行的代码,主要功能为判断手指单击到的是什么按钮,若是碰到重新开始按钮就重新加载本场景,若是碰到静音按钮就更改静态类中的静音标志位yinyue以及静音按钮的图案,若单击的是退出按钮就回到游戏的主菜单界面。
上一小节介绍了游戏中控制板的开发,本小节将详细介绍游戏结束后显示的分数板相关脚本的开发过程,以实现分数板上显示分数、星星数的效果,以及分数板下的“进入下一关”按钮的按键监听。具体步骤如下。
(1)新建脚本“ScoreBoard.cs”并挂载到分数板预制件上。该脚本的主要功能是在分数板上呈现本局游戏所获得的分数,以及分数板上的按钮监听。脚本代码如下。
代码位置:见随书光盘中源代码/第06章目录下的ColaCola/Assets/script/ScoreBoard.cs。
1 using UnityEngine;
2 using System.Collections;
3 public class ScoreBoard : MonoBehaviour {
4 public GameObject gewei; //总分
5 public GameObject shiwei; //十位分数
6 public GameObject baiwei; //百位分数
7 public GameObject qianwei; //千位分数
8 public GameObject[] mystar; //星星板
9 public GameObject zhubeijing; //主背景
10 public Material[] mat; //数字材质
11 public Material star; //星星材质
12 public Material picnull; //透明图片
13 void Start () {
14 int zhu = zhubeijing.renderer.material.renderQueue; //主背景的渲染排序
15 int score = MyStaticClass.getzongfen(); //取出总分数
16 gewei.renderer.material = mat[score%10]; //设置分数
17 shiwei.renderer.material = mat[score%100-score%10]; //赋予十位数材质
18 baiwei.renderer.material = mat[(int)(score%1000/100)]; //赋予百位数材质
19 if ((int)score / 1000 == 0) { //最大不到千位
20 qianwei.renderer.material = picnull; //将千位设置为透明材质
21 }
22 else{
23 qianwei.renderer.material = mat[(int)score / 1000]; //赋予千位材质
24 }
25 if (MyStaticClass.shengfu == 1) { //游戏胜利
26 int xingxingshu = MyStaticClass.shengqiu + 1; //不剩1星剩一个2星两个3星
27 if (xingxingshu >= 3){ //星星数超过3为
28 xingxingshu = 3; //星星数
29 }
30 if (Application.loadedLevelName == "Level1") { //按照不同关卡分别存分数
31 PlayerPrefs.SetInt("level1score", Mathf.Max(PlayerPrefs.GetInt("level1score"),xingxingshu));
32 }
33 else if (Application.loadedLevelName == "Level2") { //按照不同关卡分别存分数
34 PlayerPrefs.SetInt("level2score", Mathf.Max(PlayerPrefs.GetInt("level2score"),xingxingshu));
35 }
36 else if (Application.loadedLevelName == "Level3"){ //按照不同关卡分别存分数
37 PlayerPrefs.SetInt("level3score", Mathf.Max(PlayerPrefs.GetInt("level3score"),xingxingshu));
38 }
39 else if (Application.loadedLevelName == "Level4") { //按照不同关卡分别存分数
40 PlayerPrefs.SetInt("level4score", Mathf.Max(PlayerPrefs.GetInt("level4score"),xingxingshu));
41 }
42 mystar[0].renderer.material = star; //为首个星星板赋予星星材质
43 mystar[0].renderer.material.renderQueue = zhu + 1; //在主背景渲染后渲染星星
44 if (MyStaticClass.shengqiu >= 2) { //剩余球大于两个
45 for (int i = 1; i < 3; i++){ //遍历星星数组
46 mystar[i].renderer.material = star; //为星星板赋予材质
47 mystar[i].renderer.material.renderQueue = zhu + 1;//在主背景渲染后渲染星星
48 }}
49 else if (MyStaticClass.shengqiu >= 1){ //剩余球的数量大于1
50 mystar[1].renderer.material = star; //为首个星星板赋予星星材质
51 mystar[1].renderer.material.renderQueue = zhu + 1; //在主背景渲染后渲染星星
52 }}else if(MyStaticClass.shengfu==2) { //游戏失败
53 for (int i = 0; i < mystar.Length;i++){ //遍历星星板
54 mystar[i].renderer.material=picnull; //为星星板赋予透明材质
55 mystar[i].renderer.material.renderQueue = zhu + 1; //在主背景渲染后渲染星星
56 }}
57 gewei.renderer.material.renderQueue = zhu + 1; //在主背景渲染后渲染分数
58 shiwei.renderer.material.renderQueue = zhu + 1; //在主背景渲染后渲染分数
59 baiwei.renderer.material.renderQueue = zhu + 1; //在主背景渲染后渲染分数
60 qianwei.renderer.material.renderQueue = zhu + 1; //在主背景渲染后渲染分数
61 }}
□ 第1行~第12行为变量声明,在这里声明了分数数字材质以及星星材质,同时还声明了分数板上用于显示分数的四个平面的游戏对象,分别代表个位、十位、百位、千位;以及用于显示星星的三个平面游戏对象,方便下面代码调用。
□ 第13行~第24行的主要功能为在游戏结束后并且已经生成分数板时,将本局得分的每一位拆分出来,并分别赋予在前面生成的四个代表个位、十位、百位、千位的平面上。
□ 第25行~第41行的功能是当游戏胜利结束时,若是获得的分数大于历史最高分,则根据当前的关卡名,将分数储存入数据库。
□ 第42行~第51行的主要功能为游戏胜利结束后,根据当前剩余球的数量计算出应该获得的星星数量,并在星星板上呈现。为了避免星星板和绘制星星的屏幕深度检测失败,这里控制星星在星星板绘制后再进行绘制。
□ 第52行~第61行的主要功能为当游戏失败时,在分数板上绘制分数,同样的放在分数板后绘制,避免深度检测失败,但不绘制星星。
(2)上面了介绍生成分数板后绘制分数与星星的方法,下面将介绍分数板触摸事件的实现,主要包含单击分数板上的进入下一关按钮进行跳转场景、单击退出按钮回到主界面、单击重新开始按钮重新加载本关。代码片段如下。
代码位置:见随书光盘中源代码/第06章目录下的ColaCola/Assets/script/ScoreBoard.cs。
1 void Update(){
2 foreach (Touch t in Input.touches) { //遍历触控事件
3 RaycastHit hit; //声明射线碰撞
4 Ray ray = Camera.main.ScreenPointToRay(t.position); //声明一条射线
5 if (t.phase == TouchPhase.Began){ //触碰开始
6 if (Physics.Raycast(ray, out hit)){ //若是发生射线碰撞
7 if (hit.transform.gameObject.name == "tuichu") { //单击退出按钮
8 Application.LoadLevel("Zhujiemian"); //加载主菜单界面
9 }
10 if (hit.transform.gameObject.name == "chongkaish"){ //单击重新开始按钮
11 MyStaticClass.gameoverflag = false; //将游戏开始标志位改为否
12 MyStaticClass.setzongfen(0); //重置得分
13 Application.LoadLevel(Application.loadedLevelName);//重新加载当前场景
14 }
15 if (hit.transform.gameObject.name == "xiayiguan"){ //单击下一关按钮
16 if (Application.loadedLevelName == "Level1"){ //如果当前是关卡1
17 MyStaticClass.gameoverflag = false; //将游戏结束标志位改为否
18 MyStaticClass.setzongfen(0); //重置总分
19 Application.LoadLevel("Level2"); //加载关卡2
20 }else if (Application.loadedLevelName == "Level2"){ //当前是关卡2
21 MyStaticClass.gameoverflag = false; //将游戏结束标志位改为否
22 MyStaticClass.setzongfen(0); //重置总分
23 Application.LoadLevel("Level3"); //加载关卡3
24 }
25 else if (Application.loadedLevelName == "Level3"){ //如果当前是关卡3
26 MyStaticClass.gameoverflag = false; //将游戏结束标志位改为否
27 MyStaticClass.setzongfen(0); //重置总分
28 Application.LoadLevel("Level4"); //加载关卡4
29 }
30 else if (Application.loadedLevelName == "Level4"){ //当前是关卡4
31 Application.LoadLevel("Zhujiemian"); //加载主菜单界面
32 }}}}}}
□ 第1行~第6行的主要功能为当发生触摸事件时,在触摸点声明一条垂直于摄像机射出射线的射线ray,若是手指触摸相位touch.phase为开始触摸且发生了射线碰撞Raycast,就证明单击到某物体,执行相应代码。
□ 第7行~第14行为若是单击退出游戏按钮,就跳转到游戏主菜单界面;若单击的是重新开始按钮,将静态类中的游戏结束标志位gameoverflag改为否,用setzongfen()方法重置静态类中的关卡得分,重新加载当前关卡。
□ 第15行~第32行的主要功能为当玩家单击的是进入下一关按钮时根据玩家当前的关卡名,加载下一关,并初始化静态类中的总分和游戏结束标志位 gameoverflag 为 false。若当前已经是最后一关就回到主菜单场景。
在游戏开发中时常要用到静态类,静态类不能被实例化,可以直接使用其属性与方法,静态类的最大特点是共享,不论是在哪一个场景都可以获得其属性,而且调用速度快,不会影响游戏的性能。所以本游戏的静态类中储存了一些设置相关的标志位、总分等。开发步骤如下。
新建一个 C#脚本,重命名为“MyStaticClass.cs”,该脚本为静态类脚本,所以不用挂载到任何对象上,其他脚本要使用静态类中的成员时可以直接用“类名.方法”或“类名.变量”的语法访问。脚本代码如下。
代码位置:见随书光盘中源代码/第06 章目录下的 ColaCola/Assets/script/MyStatic Class.cs。
1 using UnityEngine;
2 using System.Collections;
3 public static class MyStaticClass {
4 private static int zongfen=0; //总分
5 public static int shengqiu=0; //剩余球
6 public static int shengfu = 0; //0未完成 1游戏胜利 2游戏失败
7 public static bool gameoverflag = false; //游戏结束
8 public static bool gamestart = true; //游戏开始标志位
9 public static bool yinyue=true; //是否开启音效
10 public static int getzongfen(){ //获得总分方法
11 return zongfen; //返回总分
12 }
13 public static void setzongfen(int x){ //设置总分方法
14 zongfen = x; //设置总分
15 }}
说明
该脚本是静态类,不需要像其他脚本继承MonoBehaviour类,里面的成员变量和成员方法都应当是静态的,在该脚本中储存了与游戏设置、计分、状态相关的标志位与变量,在游戏运行中可以随时取出这些变量,非常方便。
至此,本案例的开发部分已经介绍完毕。本游戏基于Unity 3D 平台开发,笔者在开发过程中已经注意到游戏性能方面的表现,所以,很注意降低游戏的内存消耗量,但实际上还是有一定的优化空间。
□ 游戏界面的改进
本游戏的场景搭建使用的图片已经相当华丽,有兴趣的读者可以更换图片以达到更换的效果。另外,由于在Unity中有很多内建的着色器,本游戏使用的着色器有限,可能还有效果更佳的着色器,有兴趣的读者可以更改各个纹理材质的着色器,以改变渲染风格,进而得到很好的效果。
□ 游戏性能的进一步优化
虽然在游戏的开发中,已经对游戏的性能优化做了一部分工作,但是,游戏的开发始终还是有一部分问题,在性能优异的移动终端上,可以比较完美地运行,但是在一些低端机器上的表现没有达到预期的效果,还需要进一步优化。
□ 优化游戏模型
本游戏所用的地图模型部分是从网上下载的,然后使用3D Max进行了简单的分组处理。由于是在网上免费下载的,模型存在几点缺陷:模型贴图没有合成一张图,模型没有进行合理的分组,模型中面的共用顶点没有进行融合。