詳解CocosCreator游戲之魚群算法
前言
最近想學(xué)一下CocosCreator,于是,編輯器下載,啟動(dòng)。
眾所周知,邊寫邊學(xué)才是最快的學(xué)習(xí)方法,得寫個(gè)Demo練練手,那么寫什么呢?聽說現(xiàn)在《墨蝦探蝌》挺火的,那就抄(學(xué)習(xí)的事怎么能叫抄呢?)寫一個(gè)類似的小游戲吧!
(在《墨蝦探蝌》中,魚的位置固定,到達(dá)一定數(shù)量后玩家會(huì)升級(jí),不會(huì)出現(xiàn)一大群魚的情況,本項(xiàng)目其實(shí)和它不同,沒有升級(jí)進(jìn)化,是會(huì)有一大群魚的,每條魚也不是固定位置,而是有自己的運(yùn)動(dòng)邏輯,其實(shí)和另一個(gè)游戲更像,不過我不知道叫什么。。。)
效果展示:
正文
首先整一個(gè)玩家player:
圖片資源用的是CocosCreator官方Demo的圖片,照著官方Demo學(xué)習(xí)了一下,懶得找魚的圖片就直接把圖片拿來用了,這個(gè)項(xiàng)目目前只用了兩張圖片
有了player就得寫個(gè)player控制腳本,點(diǎn)擊一個(gè)方向,player就會(huì)一直向這個(gè)方向移動(dòng)。那么我們首先需要獲取玩家點(diǎn)擊的位置,然后計(jì)算出player移動(dòng)的方向,我們把這個(gè)寫在GameManager里面,所以新建一個(gè)腳本GameManager,這個(gè)腳本掛在Canvas上。
先定義兩個(gè)變量,玩家節(jié)點(diǎn)和方向向量:
@property(cc.Node) player: cc.Node = null; ir: cc.Vec2 = cc.Vec2.ZERO;
獲取方向的方法:
getClickDir(event) { let pos: cc.Vec2 = event.getLocation(); //轉(zhuǎn)本地坐標(biāo) let localPos = this.node.convertToNodeSpaceAR(pos); let playerPos: cc.Vec2 = new cc.Vec2( this.player.position.x, this.player.position.y ); let len = localPos.sub(playerPos).mag(); this.dir.x = localPos.sub(playerPos).x / len; this.dir.y = localPos.sub(playerPos).y / len; }
這方法在onMouseDown和onMouseMove時(shí)調(diào)用:
onMouseDown(event) { if (event.getButton() == cc.Event.EventMouse.BUTTON_LEFT) { this.getClickDir(event); } } onMouseMove(event) { if (event.getButton() == cc.Event.EventMouse.BUTTON_LEFT) { this.getClickDir(event); } } onLoad() { cc.director.getCollisionManager().enabled = true; cc.director.getPhysicsManager().enabled = true; this.node.on(cc.Node.EventType.MOUSE_DOWN, this.onMouseDown, this); this.node.on(cc.Node.EventType.MOUSE_MOVE, this.onMouseMove, this); } onDestroy() { this.node.off(cc.Node.EventType.MOUSE_DOWN, this.onMouseDown, this); this.node.off(cc.Node.EventType.MOUSE_MOVE, this.onMouseMove, this); }
有了方向向量,就可以讓玩家移動(dòng)了,新建一個(gè)FishPlayer腳本。
為了不讓玩家亂跑,我們先 build the wall:
墻上加上物理碰撞體:
然后就可以開始寫FishPlayer腳本了,先把要用到的變量都定義一下:
@property(cc.Node) camera: cc.Node = null; @property(cc.Node) gameManager: cc.Node = null; game: GameManager; speed: number = 170; velocity: cc.Vec3 = cc.Vec3.ZERO;
在onLoad()中給game賦值:
onLoad() { this.game = this.gameManager.getComponent("GameManager"); }
通過射線來檢測(cè)邊界,判斷玩家是否能移動(dòng)的方法:
canMove() { var flag: boolean = true; //前方有障礙物 var pos = this.node.convertToWorldSpaceAR(cc.Vec3.ZERO); var endPos = pos.add(this.node.up.mul(40)); var hit: cc.PhysicsRayCastResult[] = cc.director .getPhysicsManager() .rayCast( new cc.Vec2(pos.x, pos.y), new cc.Vec2(endPos.x, endPos.y), cc.RayCastType.All ); if (hit.length > 0) { flag = false; } return flag; }
在update中控制玩家移動(dòng):
update(dt) { if (this.game.dir.mag() < 0.5) { this.velocity = cc.Vec3.ZERO; return; } let vx: number = this.game.dir.x * this.speed; let vy: number = this.game.dir.y * this.speed; this.velocity = new cc.Vec3(vx, vy); //移動(dòng) if (this.canMove()) { this.node.x += vx * dt; this.node.y += vy * dt; } //相機(jī)跟隨 this.camera.setPosition(this.node.position); //向運(yùn)動(dòng)方向旋轉(zhuǎn) let hudu = Math.atan2(this.game.dir.y, this.game.dir.x); let angle = hudu * (180 / Math.PI); angle = 360 - angle + 90; this.node.angle = -angle; }
玩家的移動(dòng)邏輯寫完了,接下來寫魚群。
新建一個(gè)FishGroupManager腳本和一個(gè)FishGroup腳本,F(xiàn)ishGroupManager掛在Canvas上,F(xiàn)ishGroup掛在player上。
FishGroupManager中定義一個(gè)靜態(tài)fishGroups變量,用來管理所有Group(因?yàn)閳?chǎng)景中可能有多個(gè)玩家,多個(gè)魚群,現(xiàn)在只有一個(gè)玩家,這里方便之后擴(kuò)展):
static fishGroups: FishGroup[]; //所有組
來一個(gè)把group加入groups的靜態(tài)方法:
static AddGroup(group: FishGroup) { if (this.fishGroups == null) this.fishGroups = new Array(); if (this.fishGroups.indexOf(group) == -1) this.fishGroups.push(group); }
再來一個(gè)獲取group的靜態(tài)方法(根據(jù)索引獲取):
static GetFishGroup(index: number) { for (var i = 0; i < this.fishGroups.length; i++) if (this.fishGroups[i].groupID == index) return this.fishGroups[i]; }
FishGroupManager就寫完了,接下來再寫FishGroup,把上面用到的groupID定義一下,還有魚群數(shù)組:
groupID: number = 0; //組id fishArr: cc.Component[] = new Array<cc.Component>();
在onLoad中將自身加到fishGroups里面:
onLoad() { FishGroupManager.AddGroup(this); }
現(xiàn)在魚群有了,但是里面一條魚都沒有,所以我們還要一個(gè)抓魚的方法:
catchFish(fish) { this.fishArr.push(fish); }
再定義一些要用到的參數(shù),F(xiàn)ishGroup就寫完了:
keepMinDistance: number = 80; keepMaxDistance: number = 100; keepWeight: number = 1; //成員保持距離和保持距離權(quán)重 moveWeight: number = 0.8; //和成員移動(dòng)權(quán)重
接下來就到了重頭戲了——魚群中其他小魚的運(yùn)動(dòng)邏輯。
直接將player復(fù)制一下,把掛載的FishPlayer和FishGroup腳本去掉,命名為fish,這就是我們的小魚了,把它做成預(yù)制。然后新建一個(gè)FishBehaviour腳本,這個(gè)腳本掛在player和普通小魚身上。
首先實(shí)現(xiàn)“抓魚”功能,當(dāng)player靠近小魚后,小魚就被捕獲,成為該player魚群中的一員。
定義相關(guān)變量:
@property(cc.Node) gameManager: cc.Node = null; game: GameManager; isPicked: boolean = false; pickRadius: number = 50; //抓取距離 groupId: number = -1; //組 id myGroup: FishGroup;
同樣,在onLoad()中給game賦值:
onLoad() { this.game = this.gameManager.getComponent(GameManager); }
判斷和player距離的方法:
getPlayerDistance() { let dist = this.node.position.sub(this.game.player.position).mag(); return dist; }
加入魚群方法:
onPicked() { //設(shè)置group this.groupId = this.game.player.getComponent(FishGroup).groupID; this.myGroup = FishGroupManager.GetFishGroup(this.groupId); if (this.myGroup != null) { this.myGroup.catchFish(this); this.isPicked = true; } }
在update中調(diào)用:
update(dt) { if (this.isPicked) { //隨著魚群移動(dòng) } else { if (this.getPlayerDistance() < this.pickRadius) { this.onPicked(); } } }
OK,現(xiàn)在小魚到魚群中了,怎么隨著魚群一起移動(dòng)呢?
這里主要有兩個(gè)點(diǎn):
1.小魚會(huì)隨著周圍“鄰居魚”一起移動(dòng)
2.小魚之間要保持距離,不能太過擁擠
所以我們需要計(jì)算小魚周圍一定范圍內(nèi)魚群運(yùn)動(dòng)向量的平均值,這樣還不夠,還要判斷是否“擁擠”,“擁擠”的話就增加一個(gè)遠(yuǎn)離的趨勢(shì),太遠(yuǎn)的話就增加一個(gè)靠近的趨勢(shì),再分別乘以權(quán)重,加起來,就可以得到我們要的向量了,代碼如下:
定義變量:
moveSpeed: number = 170; rotateSpeed: number = 40; //移動(dòng)旋轉(zhuǎn)速度 neighborRadius: number = 500; //距離小于500算是鄰居 speed: number = 0; currentSpeed: number = 0; myMovement: cc.Vec3 = cc.Vec3.ZERO;
求平均向量:
GetGroupMovement() { var v1: cc.Vec3 = cc.Vec3.ZERO; var v2: cc.Vec3 = cc.Vec3.ZERO; for (var i = 0; i < this.myGroup.fishArr.length; i++) { var otherFish: FishBehaviour = this.myGroup.fishArr[i].getComponent( FishBehaviour ); var dis = this.node.position.sub(otherFish.node.position); //距離 //不是鄰居 if (dis.mag() > this.neighborRadius) { continue; } var v: cc.Vec3 = cc.Vec3.ZERO; //大于最大間隔,靠近 if (dis.mag() > this.myGroup.keepMaxDistance) { v = dis.normalize().mul(1 - dis.mag() / this.myGroup.keepMaxDistance); } //小于最小間隔,遠(yuǎn)離 else if (dis.mag() < this.myGroup.keepMinDistance) { v = dis.normalize().mul(1 - dis.mag() / this.myGroup.keepMinDistance); } else { continue; } v1 = v1.add(v); //與周圍單位的距離 v2 = v2.add(otherFish.myMovement); //周圍單位移動(dòng)方向 } //添加權(quán)重因素 v1 = v1.normalize().mul(this.myGroup.keepWeight); v2 = v2.normalize().mul(this.myGroup.moveWeight); var ret = v1.add(v2); return ret; }
現(xiàn)在,可以把update補(bǔ)全了:
update(dt) { //隨著魚群移動(dòng) if (this.isPicked) { var direction = cc.Vec3.ZERO; if (this.node.name != "player") { direction = direction.add(this.GetGroupMovement()); } this.speed = cc.misc.lerp(this.speed, this.moveSpeed, 2 * dt); this.Drive(direction, this.speed, dt); //移動(dòng) } //捕獲 else { if (this.getPlayerDistance() < this.pickRadius) { this.onPicked(); } } }
Drive()方法:
Drive(direction: cc.Vec3, spd: number, dt) { var finialDirection: cc.Vec3 = direction.normalize(); var finialSpeed: number = spd; var finialRotate: number = 0; var rotateDir: number = cc.Vec3.dot(finialDirection, this.node.right); var forwardDir: number = cc.Vec3.dot(finialDirection, this.node.up); if (forwardDir < 0) { rotateDir = Math.sign(rotateDir); } //防抖 if (forwardDir < 0.98) { finialRotate = cc.misc.clampf( rotateDir * 180, -this.rotateSpeed, this.rotateSpeed ); } finialSpeed *= cc.misc.clamp01(direction.mag()); finialSpeed *= cc.misc.clamp01(1 - Math.abs(rotateDir) * 0.8); if (Math.abs(finialSpeed) < 0.01) { finialSpeed = 0; } //移動(dòng) if (this.canMove()) { this.node.x += this.node.up.x * finialSpeed * dt; this.node.y += this.node.up.y * finialSpeed * dt; } //旋轉(zhuǎn) var angle1 = finialRotate * 8 * dt; var angle2 = this.node.angle - angle1; this.node.angle = angle2 % 360; this.currentSpeed = finialSpeed; this.myMovement = direction.mul(finialSpeed); } canMove() { var flag: boolean = true; //前方有障礙物 var pos = this.node.convertToWorldSpaceAR(cc.Vec3.ZERO); var endPos = pos.add(this.node.up.mul(40)); var hit: cc.PhysicsRayCastResult[] = cc.director .getPhysicsManager() .rayCast( new cc.Vec2(pos.x, pos.y), new cc.Vec2(endPos.x, endPos.y), cc.RayCastType.All ); if (hit.length > 0) { flag = false; } return flag; }
以上就是詳解CocosCreator游戲之魚群算法的詳細(xì)內(nèi)容,更多關(guān)于CocosCreator魚群算法的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
從零開始用electron手?jǐn)]一個(gè)截屏工具的示例代碼
這篇文章主要介紹了從零開始用electron手?jǐn)]一個(gè)截屏工具的示例代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-10-10javascript中的nextSibling使用陷(da)阱(keng)
關(guān)于HTML/XML節(jié)點(diǎn)的問題,在IE中nextSibling不會(huì)返回文本節(jié)點(diǎn),而chrome或者firefox等會(huì)返回文本節(jié)點(diǎn)2014-05-05Chrome插件(擴(kuò)展)開發(fā)全攻略(完整demo)
Chrome插件是一個(gè)用Web技術(shù)開發(fā)、用來增強(qiáng)瀏覽器功能的軟件,它其實(shí)就是一個(gè)由HTML、CSS、JS、圖片等資源組成的一個(gè).crx后綴的壓縮包,本文給大家分享一個(gè)Chrome插件(擴(kuò)展)開發(fā)全攻略完整demo,感興趣的朋友跟隨小編一起學(xué)習(xí)下吧2021-05-05使用egg.js實(shí)現(xiàn)手機(jī)、驗(yàn)證碼注冊(cè)的項(xiàng)目實(shí)踐
本文主要介紹了使用egg.js實(shí)現(xiàn)手機(jī)、驗(yàn)證碼注冊(cè)的項(xiàng)目實(shí)踐,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04JavaScript使用RegExp進(jìn)行正則匹配的方法
這篇文章主要介紹了JavaScript使用RegExp進(jìn)行正則匹配的方法,實(shí)例分析了RegExp對(duì)象在進(jìn)行正則匹配時(shí)的相關(guān)使用技巧,需要的朋友可以參考下2015-07-07