试Cocos Creator (20K~40K薪资)经常会被问的20个问题, 看看你懂几个。
1: 如何优化Cocos Creator 包体体积。
2: Cocos creator如何做资源管理。
3: Cocos Creator如何编写单例模式。
4: Cococs creator 如何使用物理引擎?
5: Cocos Creator Label的原理与如何节约Drawcall?
6: Cocos Creator 背包系统可能会需要哪些优化?
7: Cocos Creator WebSocket与Socket.IO分别是什么?
8: Cocos Creator如何内置protobuf JS版本?
9: Cocos Creator 材质, shader 分别是什么?
10:Cocos creator 固定宽度与固定高度的底层原理是什么?Cocos creator是如何做适配的?
11: Cocos Creator 图集打包有什么意义,我们一般在项目里面怎么规划图集?
12: Cocos Creator 如何做游戏框架,能让多人很好的协作,代码好维护?
13: Cocos Creator 2D如何做Drawcall优化?
14: Cococs creator 骨骼动画与帧动画的优缺点是什么?
15: 如何使用Cococs Creator制作一个地图编辑与寻路导航系统?
16: Cocos Creator 节点池的基本原理是什么?如何使用?
18: Cocos Creator 如何设计热更新系统, 如何设计大厅与子游戏模式?
19: Cocos Android里 runOnGLThread是什么意思?
20: Cocos Creator 如何设计自动化打包发布脚本?
Cocos Creator架构师,主要考的是扎实的基础,分析问题,解决问题的能力和经验,系统框架设计, 与项目难题攻关, 如果大家对哪个问题的答案感兴趣,可以评论区留言,Blake老师会根据大家的需求,来写文章回复大家问题。 祝大家早日成长为架构师。
源:新京报
从公元前1万年出现在人类食谱中起,酸奶在这千年间征服着世界各地食客的味蕾。值得一提的是,除了作为一种零食甜点,生活在不同地区的人们食用酸奶的方式并不相同。比如,77%的土耳其人习惯将酸奶与热餐搭配,55%的巴西人则喜欢用它搭配谷物,对法国人而言,酸奶甚至称得上是一种信仰。人们使用酸奶的方式,折射着不同地区的文化与习俗。
我们享用酸奶的方式,其实与我们的祖先享用酸奶的方式有着内在的联系。可以肯定的是,大多数有影响力的酸奶制作者,他们和那些传承了家族传统及食谱的人有着亲缘关系。在为英国广播公司旅行博客撰写的一篇文章中,作者玛达薇 ·拉马尼(Mad hvi Ra mani)引用了保加利亚人艾莉莎·斯托伊洛伐的一段话:
如果两位来自不同村庄的祖母用相同的原料制作酸奶,其味道也会有所不同。这是因为酸奶是一种个性化产品。它与地域、动物、家庭的特殊口味以及代代相传的制作知识有关。
时至今日,在酸奶的起源地区,它仍然是当地人日常饮食中不可或缺的一部分。然而,随着人们四处迁徙,酸奶文化也随之流传开来,这样说毫不夸张,因为对一些人来说,他们现在遵循的家族酸奶文化就是从其他地方传过来的。和酸奶相关的传统与习俗、口味与香气、制作与灵感在全球范围内得到共享,这展示了世界是如何无缝拥抱这种食品的。
保加利亚酸奶:
走向全球的酸奶菌种
为了向保加利亚乳杆菌的发源地致敬,让我们从保加利亚开始我们的全球酸奶之旅。在保加利亚酸奶的制作过程中,保加利亚乳杆菌和嗜热链球菌这两种著名的菌株协同作用,创造了酸奶制作的黄金标准,保加利亚人称这种酸奶为“kiselo mlyako”。这种酸奶具有独特的酸味、浓郁的口感和特殊的香气,酸奶爱好者很容易就能辨认出这是保加利亚酸奶。
《舔盖儿:酸奶小史》,[美]琼·赫什 著,吴岭 译,万川|中国工人出版社 ,2024年4月。
在20世纪早期和中期,来自保加利亚的菌株以冻干物或药丸的形式被兜售和运输。1937年,伦敦《观察家报》(The Observer)上刊载了一篇文章,文章报道了萨尔茨堡一家小乳品店倒闭的消息,可以说,这是保加利亚酸奶备受消费者推崇的最好证明。据说,包括意大利大师级指挥家阿尔图罗·托斯卡尼尼在内,人们涌向这家维也纳商店,享受诗歌和享用正宗的保加利亚酸奶。下面这些诗句节选自这家店主的诗歌:
为何保加利亚人如此长寿?
为何他们从不感冒?
因为,他们——
喜欢在春冬时节享用酸奶。
保加利亚酸奶属于保加利亚的国家专利,保加利亚人将其同名菌株——保加利亚乳杆菌授权给其他国家使用,如果这些国家想将自己生产的酸奶称为“保加利亚酸奶”,它们就必须从保加利亚购买这种酸奶菌种。
最好的例证来自保加利亚罗德比山脉(Rhodope Mountains)中的一个小村庄和中国。2009 年,中国的光明乳业股份有限公司推出了一款名为“莫斯利安”的常温酸奶饮品,这款酸奶所用的菌株源自保加利亚境内的一个同名村庄——莫斯利安村(Momchilovtsi)。这款酸奶的产地在上海。如果你去莫斯利安这个小村庄参观,你会在这里看到许多中文标识,还有不少自学普通话的村民。每年,这里都会举办一个盛大的节日——莫斯利安酸奶文化节, 而且还会选出一位“酸奶皇后”。这个被称为“长寿村”的小镇有1200多名居民,每年都会招待1000多名中国游客。
新疆奶酪和老北京酸奶:
“方便”概念下的情怀
新疆维吾尔自治区是中国西北部的一个自治区,这里生活着大量的维吾尔族人,酸奶在新疆的出现,表明这里是全球不同饮食传统的另一个交会点,它也影响着中国文化。维吾尔族人已经在这里生活了1000多年。与汉族相比,他们的烹饪更接近中东风味,“奶酪”酸奶在这里非常受欢迎。
正如美食作家范(Van)在她的个人美食网站中所写的那样:
奶酪可以说是早期的酸奶。最早在19世纪时,宫廷厨师们掌握了这道甜点,后来,奶酪的配方发生演变,它的味道变得更柔和、更甜。在20世纪50年代,它开始在北京流行起来,成为注重健康的潮流人士的最爱。
装在瓷瓶中的老北京特色酸奶,采用传统的蓝白色薄 纸盖包装,还配有吸管。(出版社供图)
渐渐地,这一食谱在全中国传播开来,如今,在每个集市和繁华街道的小贩那里,都有老北京酸奶销售。边喝老北京酸奶边在市场上漫步,可以说是一种享受。老北京酸奶装在瓷瓶中,封口是系着绳子的蓝白色薄纸盖,吃酸奶时,用细吸管或一次性勺子插进去,然后就可以好好享用了,吃完后,记得再将酸奶瓶归还给小贩。
如今,中国消费者更多地生活在城市,他们有了更多的可支配收入,也在寻找便携式的营养来源。“方便食品或便携食品”(food on-the-go)这一饮食概念在亚洲市场非常具有吸引力,因此,在中国和韩国,你会发现在湍急的人流中,有女性骑着自行车兜售酸奶。
韩国的“酸奶女士”穿着标志性的杏色夹克,戴着粉色头盔。她们驾驶着名为“CoCos”的电动冰箱,是“Cold & Cool”的缩写。这些带轮子的冰箱可以容纳数量惊人的3300瓶酸奶。在亚洲烹饪文化中,酸奶仍未被视为一种烹饪辅料或主流食品,而是被人们当作一种快速补充营养的方式。中国和东南亚市场有望成为全球最大的酸奶消费市场,其中,饮用型酸奶有助于推动市场发展。
日本酸奶:益生菌的由来
发现并重新认识酸奶的并不只有中国,其他亚洲市场也是如此。日本对酸奶的热爱始于2 0世纪30年代,当时,一位出生于京都的科学家——代田稔博士(Dr Minora Shirota),对乳酸杆菌与疾病之间的关系进行了探索。经过一番详尽的研究之后,他分离出了干酪乳杆菌代田株。这是一种由300多种乳酸杆菌组成的益生菌,代田稔博士用它来发酵牛奶,并将得到的酸奶产品命名为养乐多。他发明的“养乐多”酸奶和他所说的“只有肠道健康才能延年益寿”的观点受到了日本民众的欢迎和支持。
货架上的“养乐多”。
时至今日,养乐多在日本仍然广受欢迎,全球每天有3000多万人享用这种酸奶,因为据说它可以提高人体免疫力,促进肠道消化。
1971年,日本乳制品企业巨头明治乳业(Meiji)推出了日本第一款原味酸奶,从而宣示其全面进入酸奶市场。同中国与保加利亚的联系一样,明治乳业意识到与保加利亚开展合作能促进酸奶的销售,于是,它在1973年获得了保加利亚的授权,推出了明治保加利亚式酸奶。明治乳业继续坚持创新,1996 年,它又获得了日本的“特定保健用食品”(Food for Specified Health Use)标签的使用权,这进一步提高了酸奶的销量。
最近,日本又推出了一系列新口味酸奶,其中包括抹茶或柿子等深受消费者欢迎的传统配料。为了进一步提升消费体验,产品采用了类似古代漆器的杯子进行包装。预计日本的酸奶市场将会继续增长,但与亚洲其他地区相比,其增长速度有所放缓。
印度次大陆酸奶:
特色菜肴的基础
在印度次大陆(包括印度、南亚和中亚的部分地区、巴基斯坦、孟加拉国和喜马拉雅山区),酸奶自古以来就是当地美食不可或缺的一部分。作为一种影响广泛的素食文化,生活在这些地区的居民将酸奶作为补充人体所需蛋白质、钙元素和脂肪的来源。此外,酸奶还是一种清凉食品,可以降低印度菜中常用香料所产生的热量。
在印度,酸奶被称为达希(dahi,印度用语,意为“凝乳”),与传统酸奶将菌种引入经过巴氏杀菌的牛奶中不同,达希是将乳酸杆菌接种在煮沸的牛奶当中。制作达希是为了促进而不是抑制凝乳的发展,这一点不同于西方的许多酸奶。在印度,制作达希是一项日常活动,将前一天做好的凝乳加入新的牛奶中,就能制作出美味浓稠的酸奶。
凝乳是许多印度特色菜肴的基础。它有助于将米饭和小扁豆汤(dal)融合在一起,这样更容易用右手抓起来食用——印度风格的吃法。“aloo palda”是加有凝乳的土豆咖喱饭,这是一道经典的帕哈里菜肴,它依靠酸奶来达到黏合食材所需要的稠度。“mor rasam”是印度南部的一种酸甜口炖菜,在这道菜肴中,用酸凝乳来制作类似酪乳的那种特色味道。“dahi papdi chaat ”是印式酸奶酥脆沙拉,它是将酸奶与各种酸辣酱(如薄荷酱、香菜酱、酸豆酱等)混合之后,作为这道广受欢迎的小吃的配料。
酸奶还可用于制作松软的南印度煎饼(dosas)以及克什米尔的招牌菜印度香饭——这是一种用慢火炖煮的食物,传统做法是将食材放在密封的厚底锅上进行烹制。除了上面这些美食,甚至还有一种印度版的烤奶酪,将酸奶、洋葱、香料和香草混合在一起,制成达希吐司(dahi toast), 在印度,这是一种很受欢迎的早餐食品。
香草味浓郁、美味可口的酸奶色拉是一种绝佳的蘸酱和调味品。(出版社供图)
在所有使用酸奶的印度菜肴中,最著名的可能是酸奶色拉(raita),它是酸奶和各种配料(如蔬菜、 水果、香草、香料等)的清凉混合物。它既可以当调味品,也可以当配菜,制作起来非常简单。拉西(lassi)酸奶奶昔是印度的国民饮料,这是一种以酸奶为基底的冰沙,其起源可以追溯到公元前1000年左右的旁遮普地区。这种饮料分咸甜两种版本:加胡椒粉或红辣椒粉即为咸味版,加芒果汁或玫瑰汁即为甜味版。
说到甜点,千万不要忽视马哈拉施特拉邦的经典甜点“shrikhand”,这道甜点简单绝妙,只需要三种配料就能制作:过滤后的酸奶、糖粉和香脆的坚果。这是一种充满风味的食物,食用时可以加入藏红花丝,或者撒上豆蔻、开心果等来提味。
在印度尼西亚,人们喜欢食用“dad iah”这种食物,它可以说是印尼版的酸奶,是将未加热的水牛奶放在竹笋中发酵而成的。在尼泊尔,人们将食用酸奶作为文化和宗教庆祝活动的一部分。尼泊尔人相信,被称为“juju dhai ”的酸奶可以带来好运。因此,在举行庆典时,人们经常会在入口处放置装满酸奶的陶罐以迎接庆祝者。现如今,不在这些国家生活的人也可以品尝到这些特色的酸奶菜肴,因为这些酸奶菜肴的烹饪方式已经与其他地区的烹饪方式相融合,并在中东、东南亚、欧洲、北美、非洲和加勒比地区广为流传。
土耳其酸奶:
作为经典配料的咸酸奶
时至今日,酸奶仍然是土耳其美食中不可或缺的一部分,这里是酸奶的发源地。酸奶是土耳其人最喜爱的配料、调味品和配菜,也是土耳其最著名的饮料——咸酸奶(ayran)的基础食材。
据说,咸酸奶是由突厥游牧民族发现的,其本质是一种含乳酸但不含酒精、加水稀释过的酸奶。在炎热的夏季,游牧民族要忍受酷热的沙漠,对他们来说,食用这种咸酸奶非常有用。它有提神补体的作用,因为按照传统咸酸奶的做法,会在酸奶中加入(能够补充能量的)盐。
传统上来说,咸酸奶是装在铜制马克杯中饮用的,它可以搭配土耳其美食饮用,有提神补体的效用。(出版社供图)
至今,土耳其人和许多生活在这一地区的其他人,他们仍然很喜欢喝咸酸奶,以至于当你走进当地的麦当劳餐厅时,你很有可能会在菜单上看到它。
土耳其人还喜欢一道与印度的酸奶色拉非常相似的菜肴,但它有着自己的地域特色。酸奶经稀释后,依次加入盐、大蒜末、黄瓜、薄荷、土茴香,通常,还会再往里加入漆树粉(sumac)、酸橙汁和橄榄油的混合物,最后得到的就是酸味小黄瓜咸奶酪汤(cacik),这是一种类似蘸酱的清新凉爽的调味品。它是土耳其许多特色菜肴(如“kebabs”和“koftas”,可以将其理解为土耳其版的烤肉串和烤肉丸)的绝佳调味品。
希腊酸奶:
酸奶黄瓜酱开胃加倍
在酸奶界,希腊酸奶可谓名声大噪,众所周知,希腊酸奶经常被人模仿。要想品尝到真正的希腊酸奶,你需要在它的发源地品尝一下“straggisto”,这是一种正宗的脱乳清酸奶。在许多希腊菜谱中,都有这种美味食品的身影,其中,最著名的当数希腊酸奶黄瓜酱(tzatz iki)。
在希腊地区,希腊酸奶黄瓜酱既可以单独作为一道开胃菜,也可以用作做饭时的酱汁。(出版社供图)
它的做法和酸奶色拉很像。制作方法很简单,先将沥干水分的黄瓜磨碎,再加入酸奶、 薄荷香料、橄榄油、盐和柠檬汁,搅拌均匀。待这些食材的味道充分融合后,即可食用。在许多丰盛的希腊特色美食中,希腊酸奶黄瓜酱都是绝佳的辅料。
虽然希腊酸奶没有被希腊注册为商标,但欧盟会经常制裁那些将酸奶名称标注为“希腊”的国家,称其是故意误导消费者。
据报道,在1948年,希腊总理泰米斯托克利·索福利斯临终前想吃最后一顿饭,风卷残云间,他便喝下了两杯啤酒、一碗汤,当然,还有他心爱的酸奶,这足以进一步证明酸奶在希腊的重要地位。
(本文内容系独家内容,经出版方授权整理自《舔盖儿:酸奶小史》。)
原著作者/[美]琼·赫什
整理/申璐
编辑/走走
校对/柳宝庆
ocos Creator 开发游戏的一个核心理念就是让内容生产和功能开发可以流畅的并行协作,我们在上个部分着重于处理美术内容,而接下来就是通过编写脚本来开发功能的流程,之后我们还会看到写好的程序脚本可以很容易的被内容生产者使用。
如果您从没写过程序也不用担心,我们会在教程中提供所有需要的代码,只要复制粘贴到正确的位置就可以了,之后这部分工作可以找您的程序员小伙伴来解决。下面让我们开始创建驱动主角行动的脚本吧。
创建脚本
注意: Cocos Creator 中脚本名称就是组件的名称,这个命名是大小写敏感的!如果组件名称的大小写不正确,将无法正确通过名称使用组件!
编写组件属性
在打开的 Player 脚本里已经有了预先设置好的一些代码块,如下所示:
cc.Class({ extends: cc.Component, properties: { // foo: { // // ATTRIBUTES: // default: null, // The default value will be used only when the component attaching // // to a node for the first time // type: cc.SpriteFrame, // optional, default is typeof default // serializable: true, // optional, default is true // }, // bar: { // get () { // return this._bar; // }, // set (value) { // this._bar=value; // } // }, }, // LIFE-CYCLE CALLBACKS: // onLoad () {}, start () { }, // update (dt) {}, });
我们来大概了解一下这些代码的作用。首先我们可以看到一个全局的 cc.Class() 方法,什么是 cc呢?cc 是 Cocos 的简称,Cocos 引擎的主要命名空间,引擎代码中所有的类、函数、属性和常量都在这个命名空间中定义。而 Class() 就是 cc 模块下的一个方法,这个方法用于声明 Cocos Creator 中的类。为了方便区分,我们把使用 cc.Class 声明的类叫做 CCClass。Class() 方法的参数是一个原型对象,在原型对象中以键值对的形式设定所需的类型参数,就能创建出所需要的类。
例如:
var Sprite=cc.Class({ name: "sprite" });
以上代码用 cc.Class() 方法创建了一个类型,并且赋给了 Sprite 变量。同时还将类名设为 sprite。类名用于序列化,一般可以省略。
对于 cc.Class 的详细学习可以参考 使用 cc.Class 声明类型。
现在我们回到脚本编辑器看回之前的代码,这些代码就是编写一个组件(脚本)所需的结构。具有这样结构的脚本就是 Cocos Creator 中的 组件(Component),他们能够挂载到场景中的节点上,提供控制节点的各种功能。我们先来设置一些属性,然后看看怎样在场景中调整他们。
找到 Player 脚本里的 properties 部分,将其改为以下内容并保存:
// Player.js //... properties: { // 主角跳跃高度 jumpHeight: 0, // 主角跳跃持续时间 jumpDuration: 0, // 最大移动速度 maxMoveSpeed: 0, // 加速度 accel: 0, }, //...
Cocos Creator 规定一个节点具有的属性都需要写在 properties 代码块中,这些属性将规定主角的移动方式,在代码中我们不需要关心这些数值是多少,因为我们之后会直接在 属性检查器 中设置这些数值。以后在游戏制作过程中,我们可以将需要随时调整的属性都放在 properties 中。
现在我们可以把 Player 组件添加到主角节点上。在 层级管理器 中选中 Player 节点,然后在 属性检查器 中点击 添加组件 按钮,选择 添加用户脚本组件 -> Player,为主角节点添加 Player 组件。
现在我们可以在 属性检查器 中(需要选中 Player 节点)看到刚添加的 Player 组件了,按照下图将主角跳跃和移动的相关属性设置好:
这些数值除了 jumpDuration 的单位是秒之外,其他的数值都是以像素为单位的,根据我们现在对 Player组件的设置:我们的主角将能够跳跃 200 像素的高度,起跳到最高点所需的时间是 0.3 秒,最大水平方向移动速度是 400 像素每秒,水平加速度是 350 像素每秒。
这些数值都是建议,一会等游戏运行起来后,您完全可以按照自己的喜好随时在 属性检查器 中修改这些数值,不需要改动任何代码。
编写跳跃和移动代码
下面我们添加一个方法,来让主角跳跃起来,在 properties: {...}, 代码块的下面,添加叫做 setJumpAction 的方法:
// Player.js properties: { //... }, setJumpAction: function () { // 跳跃上升 var jumpUp=cc.moveBy(this.jumpDuration, cc.v2(0, this.jumpHeight)).easing(cc.easeCubicActionOut()); // 下落 var jumpDown=cc.moveBy(this.jumpDuration, cc.v2(0, -this.jumpHeight)).easing(cc.easeCubicActionIn()); // 不断重复 return cc.repeatForever(cc.sequence(jumpUp, jumpDown)); },
这里就需要了解一下 Cocos Creator 的 动作(Action)系统 了。由于动作系统比较复杂,这里就简单的介绍一下。
在 Cocos Creator 中,动作 简单来说就是 节点的位移、缩放和旋转。
例如在上面的代码中,moveBy() 方法的作用是在规定的时间内移动指定的一段距离,第一个参数就是我们之前定义主角属性中的跳跃时间,第二个参数是一个 Vec2(表示 2D 向量和坐标)类型的对象,为了更好的理解,我们可以看看官方给的函数说明:
/** * !#en * Moves a Node object x,y pixels by modifying its position property. <br/> * x and y are relative to the position of the object. <br/> * Several MoveBy actions can be concurrently called, and the resulting <br/> * movement will be the sum of individual movements. * !#zh 移动指定的距离。 * @method moveBy * @param {Number} duration duration in seconds * @param {Vec2|Number} deltaPos * @param {Number} [deltaY] * @return {ActionInterval} * @example * // example * var actionTo=cc.moveBy(2, cc.v2(windowSize.width - 40, windowSize.height - 40)); */ cc.moveBy=function (duration, deltaPos, deltaY) { return new cc.MoveBy(duration, deltaPos, deltaY); };
可以看到,方法 moveBy 一共可以传入三个参数,前两个参数我们已经知道,第三个参数是 Number 类型的 Y 坐标,我们可以发现第二个参数是可以传入两种类型的,第一种是 Number 类型,第二种才是 Vec2类型,如果我们在这里传入的是 Number 类型,那么默认这个参数就是 X 坐标,此时就要填第三个参数,为 Y 坐标。上面的例子中 cc.moveBy(this.jumpDuration, cc.v2(0, this.jumpHeight)) 第二个参数传入的是使用 cc.v2 方法构建的 Vec2 类型对象,这个类型表示的是一个坐标,即有 X 坐标也有 Y 坐标,因为不需要再传入第三个参数!同时注意官方的一段话 x and y are relative to the position of the object.,这句话的意思是传入的 X、Y 坐标都是相对于节点当前的坐标位置,而不是整个坐标系的绝对坐标。
了解了参数的含义之后,我们再来关注 moveBy() 方法的返回值,看官方说明可以知道,这个方法返回的是一个 ActionInterval 类型的对象,ActionInterval 在 Cocos 中是一个表示时间间隔动作的类,这种动作在一定时间内完成。到这里我们就可以理解代码 cc.moveBy(this.jumpDuration, cc.v2(0, this.jumpHeight)).easing(cc.easeCubicActionOut()) 前一部分 的意思了,它的意思就是构造一个 ActionInterval 类型的对象,这个对象表示在 jumpDuration 的时间内,移动到相对于当前节点的 (0,this.jumpHeight) 的坐标位置,简单来说,就是一个向上跳跃的动作。
那么 后半部分 easing(cc.easeCubicActionOut()) 的作用是什么呢?easing 是 ActionInterval 类下的一个方法,这个方法可以让时间间隔动作呈现为一种缓动运动,传入的参数是一个缓动对象,返回一个 ActionInterval 类型对象,这里传入的是使用 easeCubicActionInOut 方法构建的缓动对象,EaseCubicInOut 是按三次函数缓动进入并退出的动作,具体曲线可参考下图:
详细内容可参考 API。
接下来在 onLoad 方法里调用刚添加的 setJumpAction 方法,然后执行 runAction 来开始动作:
// Player.js onLoad: function () { // 初始化跳跃动作 this.jumpAction=this.setJumpAction(); this.node.runAction(this.jumpAction); },
onLoad 方法会在场景加载后立刻执行,所以我们会把初始化相关的操作和逻辑都放在这里面。我们首先将循环跳跃的动作传给了 jumpAction 变量,之后调用这个组件挂载的节点下的 runAction 方法,传入循环跳跃的 Action 从而让节点(主角)一直跳跃。保存脚本,然后我们就可以开始第一次运行游戏了!
点击 Cocos Creator 编辑器上方正中的 预览游戏 按钮
,Cocos Creator 会自动打开您的默认浏览器并开始在里面运行游戏,现在应该可以看到我们的主角——紫色小怪兽在场景中间活泼的蹦个不停了。
移动控制
只能在原地傻蹦的主角可没前途,让我们为主角添加键盘输入,用 A 和 D 来控制他的跳跃方向。在 setJumpAction 方法的下面添加键盘事件响应函数:
// Player.js setJumpAction: function () { //... }, onKeyDown (event) { // set a flag when key pressed switch(event.keyCode) { case cc.macro.KEY.a: this.accLeft=true; break; case cc.macro.KEY.d: this.accRight=true; break; } }, onKeyUp (event) { // unset a flag when key released switch(event.keyCode) { case cc.macro.KEY.a: this.accLeft=false; break; case cc.macro.KEY.d: this.accRight=false; break; } },
然后修改 onLoad 方法,在其中加入向左和向右加速的开关,以及主角当前在水平方向的速度。最后再调用 cc.systemEvent,在场景加载后就开始监听键盘输入:
// Player.js onLoad: function () { // 初始化跳跃动作 this.jumpAction=this.setJumpAction(); this.node.runAction(this.jumpAction); // 加速度方向开关 this.accLeft=false; this.accRight=false; // 主角当前水平方向速度 this.xSpeed=0; // 初始化键盘输入监听 cc.systemEvent.on(cc.SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this); cc.systemEvent.on(cc.SystemEvent.EventType.KEY_UP, this.onKeyUp, this); }, onDestroy () { // 取消键盘输入监听 cc.systemEvent.off(cc.SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this); cc.systemEvent.off(cc.SystemEvent.EventType.KEY_UP, this.onKeyUp, this); },
有 Android 开发经验的同学比较好理解,这里的监听器实质上就和 Android 里的 OnClickListener 差不多,在 cocos 中通过 systemEvent 来监听系统 全局 事件。(鼠标、触摸和自定义事件的监听和派发的详细内容请参考 监听和发射事件。)这里通过向 systemEvent 注册了一个键盘响应函数,在函数中通过 switch 判断键盘上的 A 和 D 是否被按下或松开,若按下就执行对应的操作。
最后修改 update 方法的内容,添加加速度、速度和主角当前位置的设置:
// Player.js update: function (dt) { // 根据当前加速度方向每帧更新速度 if (this.accLeft) { this.xSpeed -=this.accel * dt; } else if (this.accRight) { this.xSpeed +=this.accel * dt; } // 限制主角的速度不能超过最大值 if ( Math.abs(this.xSpeed) > this.maxMoveSpeed ) { // if speed reach limit, use max speed with current direction this.xSpeed=this.maxMoveSpeed * this.xSpeed / Math.abs(this.xSpeed); } // 根据当前速度更新主角的位置 this.node.x +=this.xSpeed * dt; },
update 在场景加载后就会每帧调用一次,我们一般把需要经常计算或及时更新的逻辑内容放在这里。在我们的游戏中,根据键盘输入获得加速度方向后,就需要每帧在 update 中计算主角的速度和位置。
保存脚本后,点击 预览游戏 来看看我们最新的成果。在浏览器打开预览后,用鼠标点击一下游戏画面(这是浏览器的限制,要点击游戏画面才能接受键盘输入),然后就可以按 A 和 D 键来控制主角左右移动了!
感觉移动起来有点迟缓?主角跳的不够高?希望跳跃时间长一些?没问题,这些都可以随时调整。只要为 Player 组件设置不同的属性值,就可以按照您的想法调整游戏。这里有一组设置可供参考:
Jump Height: 150 Jump Duration: 0.3 Max Move Speed: 400 Accel: 1000
这组属性设置会让主角变得灵活无比,至于如何选择,就看您想做一个什么风格的游戏了。
制作星星
主角现在可以跳来跳去了,我们要给玩家一个目标,也就是会不断出现在场景中的星星,玩家需要引导小怪兽碰触星星来收集分数。被主角碰到的星星会消失,然后马上在随机位置重新生成一个。
制作 Prefab
对于需要重复生成的节点,我们可以将他保存成 Prefab(预制) 资源,作为我们动态生成节点时使用的模板。关于 Prefab 的更多信息,请阅读 预制资源(Prefab)。
首先从 资源管理器 中拖拽 assets/textures/star 图片到场景中,位置随意,我们只是需要借助场景作为我们制作 Prefab 的工作台,制作完成后会我们把这个节点从场景中删除。
我们不需要修改星星的位置或渲染属性,但要让星星能够被主角碰触后消失,我们需要为星星也添加一个专门的组件。按照和添加 Player 脚本相同的方法,添加名叫 Star 的 JavaScript 脚本到 assets/scripts/中。
接下来双击这个脚本开始编辑,星星组件只需要一个属性用来规定主角距离星星多近时就可以完成收集,修改 properties,加入以下内容并保存脚本。
// Star.js properties: { // 星星和主角之间的距离小于这个数值时,就会完成收集 pickRadius: 0, },
将这个脚本添加到刚创建的 star 节点上,在 层级管理器 中选中 star 节点,然后在 属性检查器 中点击 添加组件 按钮,选择 添加用户脚本组件 -> Star,该脚本便会添加到刚创建的 star 节点上。然后在 属性检查器中把 Pick Radius 属性值设为 60:
Star Prefab 需要的设置就完成了,现在从 层级管理器 中将 star 节点拖拽到 资源管理器 中的 assets 文件夹下,就生成了名叫 star 的 Prefab 资源。
现在可以从场景中删除 star 节点了,后续可以直接双击这个 star Prefab 资源进行编辑。
接下去我们会在脚本中动态使用星星的 Prefab 资源生成星星。
添加游戏控制脚本
星星的生成是游戏主逻辑的一部分,所以我们要添加一个叫做 Game 的脚本作为游戏主逻辑脚本,这个脚本之后还会添加计分、游戏失败和重新开始的相关逻辑。
添加 Game 脚本到 assets/scripts 文件夹下,双击打开脚本。首先添加生成星星需要的属性:
// Game.js properties: { // 这个属性引用了星星预制资源 starPrefab: { default: null, type: cc.Prefab }, // 星星产生后消失时间的随机范围 maxStarDuration: 0, minStarDuration: 0, // 地面节点,用于确定星星生成的高度 ground: { default: null, type: cc.Node }, // player 节点,用于获取主角弹跳的高度,和控制主角行动开关 player: { default: null, type: cc.Node } },
这里初学者可能会疑惑,为什么像 starPrefab 这样的属性会用 {} 括起来,括号里面还有新的 “属性” 呢?其实这是属性的一种完整声明,之前我们的属性声明都是不完整的,有些情况下,我们需要为属性声明添加参数,这些参数控制了属性在 属性检查器 中的显示方式,以及属性在场景序列化过程中的行为。例如:
properties: { score: { default: 0, displayName: "Score (player)", tooltip: "The score of player", } }
以上代码为 score 属性设置了三个参数 default、 displayName 和 tooltip。这几个参数分别指定了 score的默认值(default)为 0,在 属性检查器 里,其属性名(displayName)将显示为 Score (player),并且当鼠标移到参数上时,显示对应的 tooltip。
下面是常用参数:
default:设置属性的默认值,这个默认值仅在组件第一次添加到节点上时才会用到
type:限定属性的数据类型,详见 CCClass 进阶参考:type 参数
visible:设为 false 则不在属性检查器面板中显示该属性
serializable: 设为 false 则不序列化(保存)该属性
displayName:在属性检查器面板中显示成指定名字
tooltip:在属性检查器面板中添加属性的 tooltip
所以上面的代码:
starPrefab: { default: null, type: cc.Prefab },
就容易理解了,首先在 Game 组件下声明了 starPrefab 属性,这个属性默认值为 null,能传入的类型必须是 Prefab 预制资源类型。这样之后的 ground、player 属性也可以理解了。
保存脚本后将 Game 组件添加到 层级管理器 中的 Canvas 节点上(选中 Canvas 节点后,拖拽脚本到 属性检查器 上,或者点击 属性检查器 的 添加组件 按钮,并从 添加用户脚本组件 中选择 Game。)
接下来从 资源管理器 中拖拽 star 的 Prefab 资源到 Game 组件的 Star Prefab 属性中。这是我们第一次为属性设置引用,只有在属性声明时规定 type 为引用类型时(比如我们这里写的 cc.Prefab 类型),才能够将资源或节点拖拽到该属性上。
接着从 层级管理器 中拖拽 ground 和 Player 节点到 Canvas 节点 Game 组件中相对应名字的属性上,完成节点引用。
然后设置 Min Star Duration 和 Max Star Duration 属性的值为 3 和 5,之后我们生成星星时,会在这两个之间随机取值,就是星星消失前经过的时间。
在随机位置生成星星
接下来我们继续修改 Game 脚本,在 onLoad 方法 后面 添加生成星星的逻辑:
// Game.js onLoad: function () { // 获取地平面的 y 轴坐标 this.groundY=this.ground.y + this.ground.height/2; // 生成一个新的星星 this.spawnNewStar(); }, spawnNewStar: function() { // 使用给定的模板在场景中生成一个新节点 var newStar=cc.instantiate(this.starPrefab); // 将新增的节点添加到 Canvas 节点下面 this.node.addChild(newStar); // 为星星设置一个随机位置 newStar.setPosition(this.getNewStarPosition()); }, getNewStarPosition: function () { var randX=0; // 根据地平面位置和主角跳跃高度,随机得到一个星星的 y 坐标 var randY=this.groundY + Math.random() * this.player.getComponent('Player').jumpHeight + 50; // 根据屏幕宽度,随机得到一个星星 x 坐标 var maxX=this.node.width/2; randX=(Math.random() - 0.5) * 2 * maxX; // 返回星星坐标 return cc.v2(randX, randY); },
这里需要注意几个问题:
保存脚本以后点击 预览游戏 按钮,在浏览器中可以看到,游戏开始后动态生成了一颗星星!用同样的方法,您可以在游戏中动态生成任何预先设置好的以 Prefab 为模板的节点。
添加主角碰触收集星星的行为
现在要添加主角收集星星的行为逻辑了,这里的重点在于,星星要随时可以获得主角节点的位置,才能判断他们之间的距离是否小于可收集距离,如何获得主角节点的引用呢?别忘了我们前面做过的两件事:
所以我们只要在 Game 脚本生成 Star 节点实例时,将 Game 组件的实例传入星星并保存起来就好了,之后我们可以随时通过 game.player 来访问到主角节点。让我们打开 Game 脚本,在 spawnNewStar 方法最后面添加一句 newStar.getComponent('Star').game=this;,如下所示:
// Game.js spawnNewStar: function() { // ... // 在星星组件上暂存 Game 对象的引用 newStar.getComponent('Star').game=this; },
保存后打开 Star 脚本,现在我们可以利用 Game 组件中引用的 player 节点来判断距离了,在 onLoad 方法后面添加名为 getPlayerDistance 和 onPicked 的方法:
// Star.js getPlayerDistance: function () { // 根据 player 节点位置判断距离 var playerPos=this.game.player.getPosition(); // 根据两点位置计算两点之间距离 var dist=this.node.position.sub(playerPos).mag(); return dist; }, onPicked: function() { // 当星星被收集时,调用 Game 脚本中的接口,生成一个新的星星 this.game.spawnNewStar(); // 然后销毁当前星星节点 this.node.destroy(); },
Node 下的 getPosition() 方法 返回的是节点在父节点坐标系中的位置(x, y),即一个 Vec2 类型对象。同时注意调用 Node 下的 destroy() 方法 就可以销毁节点。
然后在 update 方法中添加每帧判断距离,如果距离小于 pickRadius 属性规定的收集距离,就执行收集行为:
// Star.js update: function (dt) { // 每帧判断和主角之间的距离是否小于收集距离 if (this.getPlayerDistance() < this.pickRadius) { // 调用收集行为 this.onPicked(); return; } },
保存脚本,再次预览测试,通过按 A 和 D 键来控制主角左右移动,就可以看到控制主角靠近星星时,星星就会消失掉,然后在随机位置生成了新的星星!
添加得分
小怪兽辛辛苦苦的收集星星,没有奖励怎么行?现在让我们来添加在收集星星时增加得分奖励的逻辑和显示。
添加分数文字(Label)
游戏开始时得分从 0 开始,每收集一个星星分数就会加 1。要显示得分,首先要创建一个 Label 节点。在 层级管理器 中选中 Canvas 节点,右键点击并选择菜单中的 创建新节点 -> 创建渲染节点 -> Label(文字),一个新的 Label 节点会被创建在 Canvas 节点下面,而且顺序在最下面。接下来我们要用如下的步骤配置这个 Label 节点:
注意: Score: 0 的文字建议使用英文冒号,因为 Label 组件的 String 属性加了位图字体后,会无法识别中文的冒号。
完成后效果如下图所示:
在 Game 脚本中添加得分逻辑
我们将会把计分和更新分数显示的逻辑放在 Game 脚本里,打开 Game 脚本开始编辑,首先在 properties 区块的 最后 添加分数显示 Label 的引用属性:
// Game.js properties: { // ... // score label 的引用 scoreDisplay: { default: null, type: cc.Label } },
接下来在 onLoad 方法 里面 添加计分用的变量的初始化:
// Game.js onLoad: function () { // ... // 初始化计分 this.score=0; },
然后在 update 方法 后面 添加名叫 gainScore 的新方法:
// Game.js gainScore: function () { this.score +=1; // 更新 scoreDisplay Label 的文字 this.scoreDisplay.string='Score: ' + this.score; },
保存 Game 脚本后,回到 层级管理器,选中 Canvas 节点,然后把前面添加好的 score 节点拖拽到 属性检查器 里 Game 组件的 Score Display 属性中。
在 Star 脚本中调用 Game 中的得分逻辑
下面打开 Star 脚本,在 onPicked 方法中加入 gainScore 的调用:
// Star.js onPicked: function() { // 当星星被收集时,调用 Game 脚本中的接口,生成一个新的星星 this.game.spawnNewStar(); // 调用 Game 脚本的得分方法 this.game.gainScore(); // 然后销毁当前星星节点 this.node.destroy(); },
保存后预览,可以看到现在收集星星时屏幕正上方显示的分数会增加了!
失败判定和重新开始
现在我们的游戏已经初具规模,但得分再多,不可能失败的游戏也不会给人成就感。现在让我们加入星星定时消失的行为,而且让星星消失时就判定为游戏失败。也就是说,玩家需要在每颗星星消失之前完成收集,并不断重复这个过程完成玩法的循环。
为星星加入计时消失的逻辑
打开 Game 脚本,在 onLoad 方法的 spawnNewStar 调用 之前 加入计时需要的变量声明:
// Game.js onLoad: function () { // ... // 初始化计时器 this.timer=0; this.starDuration=0; // 生成一个新的星星 this.spawnNewStar(); // 初始化计分 this.score=0; },
然后在 spawnNewStar 方法最后加入重置计时器的逻辑,其中 this.minStarDuration 和 this.maxStarDuration 是我们一开始声明的 Game 组件属性,用来规定星星消失时间的随机范围:
// Game.js spawnNewStar: function() { // ... // 重置计时器,根据消失时间范围随机取一个值 this.starDuration=this.minStarDuration + Math.random() * (this.maxStarDuration - this.minStarDuration); this.timer=0; },
在 update 方法中加入计时器更新和判断超过时限的逻辑:
// Game.js update: function (dt) { // 每帧更新计时器,超过限度还没有生成新的星星 // 就会调用游戏失败逻辑 if (this.timer > this.starDuration) { this.gameOver(); return; } this.timer +=dt; },
最后,在 gainScore 方法后面加入 gameOver 方法,游戏失败时重新加载场景。
// Game.js gameOver: function () { this.player.stopAllActions(); //停止 player 节点的跳跃动作 cc.director.loadScene('game'); }
这里需要初学者了解的是,cc.director 是一个管理你的游戏逻辑流程的单例对象。由于 cc.director 是一个单例,你不需要调用任何构造函数或创建函数,使用它的标准方法是通过调用 cc.director.methodName(),例如这里的 cc.director.loadScene('game') 就是重新加载游戏场景 game,也就是游戏重新开始。而节点下的 stopAllActions 方法就显而易见了,这个方法会让节点上的所有 Action 都失效。
以上,对 Game 脚本的修改就完成了,保存脚本,然后打开 Star 脚本,我们需要为即将消失的星星加入简单的视觉提示效果,在 update 方法最后加入以下代码:
// Star.js update: function() { // ... // 根据 Game 脚本中的计时器更新星星的透明度 var opacityRatio=1 - this.game.timer/this.game.starDuration; var minOpacity=50; this.node.opacity=minOpacity + Math.floor(opacityRatio * (255 - minOpacity)); }
保存 Star 脚本,我们的游戏玩法逻辑就全部完成了!现在点击 预览游戏 按钮,我们在浏览器看到的就是一个有核心玩法、激励机制、失败机制的合格游戏了。
加入音效
尽管很多人玩手游的时候会无视声音,我们为了教程展示的工作流程尽量完整,还是要补全加入音效的任务。
跳跃音效
首先加入跳跃音效,打开 Player 脚本,添加引用声音文件资源的 jumpAudio 属性:
// Player.js properties: { // ... // 跳跃音效资源 jumpAudio: { default: null, type: cc.AudioClip }, },
然后改写 setJumpAction 方法,插入播放音效的回调,并通过添加 playJumpSound 方法来播放声音:
// Player.js setJumpAction: function () { // 跳跃上升 var jumpUp=cc.moveBy(this.jumpDuration, cc.v2(0, this.jumpHeight)).easing(cc.easeCubicActionOut()); // 下落 var jumpDown=cc.moveBy(this.jumpDuration, cc.v2(0, -this.jumpHeight)).easing(cc.easeCubicActionIn()); // 添加一个回调函数,用于在动作结束时调用我们定义的其他方法 var callback=cc.callFunc(this.playJumpSound, this); // 不断重复,而且每次完成落地动作后调用回调来播放声音 return cc.repeatForever(cc.sequence(jumpUp, jumpDown, callback)); }, playJumpSound: function () { // 调用声音引擎播放声音 cc.audioEngine.playEffect(this.jumpAudio, false); },
这里需要强调的是回调函数的作用,我们首先来看官方对 callFunc() 方法的定义:
/** * !#en Creates the action with the callback. * !#zh 执行回调函数。 * @method callFunc * @param {function} selector * @param {object} [selectorTarget=null] * @param {*} [data=null] - data for function, it accepts all data types. * @return {ActionInstant} * @example * // example * // CallFunc without data * var finish=cc.callFunc(this.removeSprite, this); * * // CallFunc with data * var finish=cc.callFunc(this.removeFromParentAndCleanup, this._grossini, true); */ cc.callFunc=function (selector, selectorTarget, data) { return new cc.CallFunc(selector, selectorTarget, data); };
我们可以看到 callFunc 方法可以传入三个参数,第一个参数是方法的 selector,我们可以理解为方法名。第二个参数是 Object 类型,一般填入 this。第三个参数为带回的数据,可以是所有的数据类型,可以不填。我们再注意到这个方法的返回值 —— ActionInstant,这是一个瞬间执行的动作类。到这里我们就可以理解了,使用 callFunc 调用回调函数可以让函数转变为 cc 中的 Action(动作),这一用法在 cc 的动作系统里非常实用!例如在上面我们将播放声音的函数传入 callFunc 赋值给 callback,让 callback 成为了一个播放声音的动作 Action,那么我们之后就能通过 cc.sequence 将跳跃和播放声音的动作组合起来,实现每跳一次就能播放音效的功能!
得分音效
保存 Player 脚本以后打开 Game 脚本,来添加得分音效,首先仍然是在 properties 中添加一个属性来引用声音文件资源:
// Game.js properties: { // ... // 得分音效资源 scoreAudio: { default: null, type: cc.AudioClip } },
然后在 gainScore 方法里插入播放声音的代码:
// Game.js gainScore: function () { this.score +=1; // 更新 scoreDisplay Label 的文字 this.scoreDisplay.string='Score: ' + this.score.toString(); // 播放得分音效 cc.audioEngine.playEffect(this.scoreAudio, false); },
保存脚本,回到 层级管理器 ,选中 Player 节点,然后从 资源管理器 里拖拽 assets/audio/jump 资源到 Player 组件的 Jump Audio 属性上。
然后选中 Canvas 节点,把 assets/audio/score 资源拖拽到 Game 组件的 Score Audio 属性上。
这样就大功告成了!完成形态的场景层级和各个关键组件的属性如下:
现在我们可以尽情享受刚制作完成的游戏了,您能打到多少分呢?别忘了您可以随时修改 Player 和 Game 组件里的移动控制和星星持续时间等游戏参数,来快速调节游戏的难度。修改组件属性之后需要保存场景,修改后的数值才会被记录下来。
总结
恭喜您完成了用 Cocos Creator 制作的第一个游戏!希望这篇快速入门教程能帮助您了解 Cocos Creator 游戏开发流程中的基本概念和工作流程。如果您对编写和学习脚本编程不感兴趣,也可以直接从完成版的项目中把写好的脚本复制过来使用。
*请认真填写需求信息,我们会在24小时内与您取得联系。