# Software Studio 2020 Spring Assignment 2 [TOC] ## Basic Components |Component|Score|Y/N| |:-:|:-:|:-:| |Membership Mechanism|10%|Y| |Complete Game Process|5%|Y| |Basic Rules|45%|Y| |Animations|10%|Y| |Sound Effects|10%|Y| |UI|10%|Y| # Website Detail Description ## Web Mario [遊戲連結](https://assignment2-mario.web.app) ### 1. 網頁開啟預設主畫面:有兩個按鈕可選擇: `Sign up` 或 `Log in` 本遊戲一定要登入才能玩,因為會**紀錄玩家狀態**,也符合本作業要求的`Membership mechanism` ![主畫面截圖](https://i.imgur.com/m6hcVm8.png) **登入**: ![](https://i.imgur.com/EYYppWI.png) **創建新帳號**: ![](https://i.imgur.com/u6Mo3QU.png) ### 2. 登入後:第一個畫面是 `Select Mode` 本遊戲有兩種模式:`One player`或`Two players` 顧名思義可以單人遊玩,或是雙人遊玩。 ![模式選擇](https://i.imgur.com/2HdVGul.png) 選擇**mode**之後,是選擇**Level** 建議從level 1 開始,分數會比較高。 ![](https://i.imgur.com/9RsrSzg.png) ### 3. 選擇Level後,就正式**進入遊戲啦!** 遊戲開始前,會先出現 **Rules**頁面進行規則講解, 使用者按下 `Start`按鈕後,就會正式進入遊戲囉! ![](https://i.imgur.com/y09OAVU.png) 而進入遊戲後畫面如下: ![](https://i.imgur.com/94bfx0p.png) ### 4. 破關成功後:若你從level 1 勝利,會進入level2(或你可以點選`EXIT`回到**模式選擇**頁面。 ![](https://i.imgur.com/wGxC2T5.png) 若選擇進入關卡2,則繼續破關! Level2也過完後,會進入**遊戲勝利**頁面, 此頁面會記錄你**本次**過關的資訊,如`score`, `time_spent`和你的`Best score` 可以點選下方的`Leader Board`按鈕進入**排行榜**頁面~ ![](https://i.imgur.com/UCaJOlp.png) ![](https://i.imgur.com/Ztb1HEq.png) # Basic Components Description : 1. World map : 我共繪致 **3個World map**:分別為`level1_map` `level2_map` `level1_2players_map` 使用助教資源包的**tileset**和**items**拼湊, 另外自己在網路上找免費圖片資源,加入`Pipe`和`Brick` ![](https://i.imgur.com/ayw5LZO.png) 2. Player : 玩家部分有建立**Player Prefab**,但是在level2時設計上有些**困難**,因此我選擇多使用一個`Player_level2.ts`去進行角色腳本控制。礙於本次作業時間有限,邊製作邊熟悉cocos creator的過程,我承認在**Game management**部分做得不太好(太多東西擠在`Player.ts`控制了,導致程式馬太冗長,也難以維護) 在**Final Project**部分我會好好與組員規劃設計,讓程式碼**乾淨切分區塊**,使的遊戲**易於開發與維護**。這樣才是能夠長久開發的遊戲! (1) **One Player mode:** 單一玩家模式:玩家可使用鍵盤的**左、右**控制玩家往左、往右移動;鍵盤**空白鍵**可以**跳躍**。 (2) **Two Player mode:** 雙人玩家模式: **第一玩家**:可使用鍵盤的**左、右**控制玩家往左、往右移動;鍵盤**空白鍵**可以**跳躍**。 **第二玩家**:可使用鍵盤的**z、c**控制玩家往左、往右移動;鍵盤**a**可以**跳躍**。 3. Enemies : 本遊戲有四種敵人: 若玩家從**上方**攻擊敵人,敵人會**死去** 其他方向觸碰均為**玩家死去** (1) Turtle : ![烏龜]() 若玩家從 **上方** 踩下烏龜:烏龜會**縮成龜殼**,接著玩家可以從**左右踢它**,龜殼會邊旋轉邊移動。 (2) **goomba:** 雙人玩家模式: ![小怪獸]() (3) **Flower :** 花繪從水管跑出來,有三種**States**: `Idle` `Up` `Down` 用**時間**控制這個**Finite State Machine**,簡單講就是定時**往上**,定時**往下**。 p.s.其實可以用Action System完成,但那時候還沒學到,所以土法煉鋼控制linearVelocity XD ![花]() (4) **Flying_goomba:** 雙人玩家模式: 這個敵人在**Level_2**時才會出現,會從天空掉下來,因為是**Dynamic**型態的Rigid Body。降落後遵循一般敵人的攻擊/死亡規則。 ![飛翔怪物]() 4. Question Blocks : 我的**問號箱子**會有三`Prefabs`生成:分別為 (1) coin (2) mushroom (3) Life 吃到(1)金幣會記錄在上方UI介面, 吃到(2)蘑菇如果現在是**小馬力歐**會長大,如果已經是大馬力歐則沒反應 吃到(3)Life可以**補命**,如果已經三條則沒反應 5. Animations : (1) Player: 共**11個**動畫 ![截圖]() (2) Enemy_Turtle: 共**3個**動畫 ![截圖]() (3) Enemy_goomba: 共**1個**動畫 ![截圖]() (4) Enemy_Flower: 共**1個**動畫 ![截圖]() 6. Sound effects : [xxxx] **(1) main scene :** 1個BGM **(2) Rules scene:** 1個按下button的 sound effect **(3) mario :** 10個 sound effect: ![截圖]() **(4) Victory scene:** 1個BGM 7. UI : **1. One Player Mode:** ![截圖]() (1)Lives: 剩幾條生命 (2)Scores: 本次關卡獲得分數 (3)Time: 還剩下多少時間 (4)Level: 第幾個關卡 (5)Pause button: 暫停按鈕 **2. Two Player Mode:** ![截圖]() 與單人模式相同,但變成**兩列** 分別表示各個玩家 # Bonus Functions Description : 1. **Leader Board** : 排行榜部分我將每個user設立一個**Best_score** field去存取歷史遊戲紀錄中的**最佳成績**, 再將這些user以`Best_score`排序,藉此存取**前三名**。 ``` typescript= Draw(){ // Go search in firebase. // OrderbyChild var userRef = firebase.database().ref('Users'); userRef.orderByChild('Best_score').limitToFirst(3).once('value',(snapshot)=>{ let i =1; snapshot.forEach((item)=>{ console.log(item.child('Best_score').val()); // cc.log(i); if(i==3){ this.name1Text.string = String(item.child('Name').val()); this.score1Text.string = String(item.child('Best_score').val()); }else if(i==2){ this.name2Text.string = String(item.child('Name').val()); this.score2Text.string = String(item.child('Best_score').val()); }else{ this.name3Text.string = String(item.child('Name').val()); this.score3Text.string = String(item.child('Best_score').val()); } i++; }) }) } ``` 2. **Multi players (offline)**: 多玩家模式,我多設了一個**scene**,叫做`level_1_2player`。並在這個scene內直接放置兩個players。 :::warning 要注意的是,我這邊的設定是: **1.玩家2預設性命是3** **2.玩家2死掉並不會影響玩家1遊戲**,但是玩家1死掉**遊戲就結束了** **3.攝影機視角是跟隨玩家1**,若玩家2因為沒有跟上或擅自超出視角,**會死掉** **4.玩家2重生的位置設定為:玩家1右側的(100,0)相對位置** ::: * `Player2.ts` ``` typescript= const {ccclass, property} = cc._decorator; @ccclass export default class Player extends cc.Component { @property(cc.SpriteFrame) BigMarioSprite: cc.SpriteFrame = null; @property(cc.SpriteFrame) smallMarioSprite: cc.SpriteFrame = null; @property({type:cc.AudioClip}) GrowSound: cc.AudioClip = null; @property({type:cc.AudioClip}) ShrinkSound: cc.AudioClip = null; @property({type:cc.AudioClip}) hurtSound: cc.AudioClip = null; @property({type:cc.AudioClip}) dieSound: cc.AudioClip = null; @property({type:cc.AudioClip}) jumpSound: cc.AudioClip = null; @property({type:cc.AudioClip}) kickSound: cc.AudioClip = null; @property({type:cc.AudioClip}) stompSound: cc.AudioClip = null; @property({type:cc.AudioClip}) MushroomUp: cc.AudioClip = null; @property({type:cc.AudioClip}) coinSound: cc.AudioClip = null; @property({type:cc.AudioClip}) gameoverSound: cc.AudioClip = null; @property({type: cc.Node}) Canvas: cc.Node = null; @property({type: cc.Node}) mainCamera: cc.Node = null; @property({type: cc.Node}) Mario1: cc.Node = null; @property({type: cc.Node}) Qblocks: cc.Node[] = []; private hitQblocks :boolean[] = [false,false,false,false,false,false,false,false]; @property([cc.Prefab]) ItemPrefabs: cc.Prefab[] = []; // 0:M 1:coin // private idleFrame: cc.SpriteFrame = null; // private anim: cc.Animation = null; private rebornPos = null; isBig: boolean = false; //defaule: small mario. lives: number = null; // private ceilingPos: number = 155; // private fallDown: boolean = false; private damageTime: number = 0; private growing: boolean = false; private shrinking: boolean = false; private dieing: boolean = false; @property(cc.Node) lifeContainer: cc.Node = null; @property(cc.Prefab) lifePrefab: cc.Prefab = null; coinText : cc.Label = null; scoreText : cc.Label = null; score : number = 0; coins : number = 0; private anim = null; //this will use to get animation component private anim_str: string = null; private animateState = null; //this will use to record animationState // private bulletPool = null; // this is a bullet manager, and it control the bullet resource private playerSpeed: number = 0; // LIFE-CYCLE CALLBACKS: // @property(cc.Prefab) // private bulletPrefab: cc.Prefab = null; private LDown: boolean = false; // key for player to go left private RDown: boolean = false; // key for player to go right // private jDown: boolean = false; // key for player to shoot private SpaceDown: boolean = false; // key for player to jump private isDead: boolean = false; private onGround: boolean = false; onLoad() { cc.director.getPhysicsManager().enabled = true;      // this.idleFrame = this.getComponent(cc.Sprite).spriteFrame; this.anim = this.getComponent(cc.Animation); this.lives = 3; //initiall lives. for(let i=1; i<=this.lives; i++){ var newlife = cc.instantiate(this.lifePrefab); newlife.name = "Life"+String(i); cc.log(newlife.name); this.lifeContainer.addChild(newlife); newlife.scaleX = 0.3; newlife.scaleY = 0.3; newlife.setPosition(50 +30*(i-1) , 0); } } start(){ cc.systemEvent.on(cc.SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this); cc.systemEvent.on(cc.SystemEvent.EventType.KEY_UP, this.onKeyUp, this); this.coinText = cc.find("Canvas/Main Camera/level1/TopContent/Coins2/number").getComponent(cc.Label); this.scoreText = cc.find("Canvas/Main Camera/level1/TopContent/Score2/value").getComponent(cc.Label); // this.animateState = this.anim.play('idle'); // Initial animation this.anim.play('idle'); // this.playerAnimation('idle'); // this.anim.play('idle'); this.rebornPos = this.node.position; // Reset Q blocks to false. for(let k=0; k<=7; k++){ this.hitQblocks[k] = false; } // this.level_1.node.getComponent('level_1').remain_lives = 3; // this.level_1.getComponent("level_1").updateLife(this.lives); } onKeyDown(event) { // cc.log("Key Down: " + event.keyCode); if(event.keyCode == cc.macro.KEY.z) { if(this.isBig)this.anim.play('Big_move'); else this.anim.play('move'); this.LDown = true; this.RDown = false; } else if(event.keyCode == cc.macro.KEY.c) { if(this.isBig)this.anim.play('Big_move'); else this.anim.play('move'); this.RDown = true; this.LDown = false; } else if(event.keyCode == cc.macro.KEY.a) { ///////// TODO : BIG-JUMP animation. ////////// if(this.isBig)this.anim.play('Big_jump'); else this.anim.play('mario_jump'); this.SpaceDown = true; } } onKeyUp(event) { if(event.keyCode == cc.macro.KEY.z){ // this.animateState = this.anim.stop('move'); if(this.isBig) this.anim.play('Big_idle'); else this.anim.play('idle'); this.LDown = false; } else if(event.keyCode == cc.macro.KEY.c){ if(this.isBig) this.anim.play('Big_idle'); else this.anim.play('idle'); this.RDown = false; } else if(event.keyCode == cc.macro.KEY.a) // if(this.isBig) this.anim.play('Big_idle'); // else this.anim.play('idle'); this.SpaceDown = false; } private playerMovement(dt) { this.playerSpeed = 0; if(this.isDead) { cc.log('Player2 die.'); this.LDown = false; this.RDown = false; this.SpaceDown = false; this.lives -- ; this.anim.play('mario_die'); this.node.position = this.rebornPos; this.isDead = false; this.getComponent(cc.RigidBody).linearVelocity = cc.v2(0, 1500); this.node.getComponent(cc.PhysicsBoxCollider).enabled = false; // Hide this node avoid multiple contact(DEAD). // this.playerAnimation('mario_die'); this.anim.play('mario_die'); this.lifeContainer.getChildByName("Life"+String(this.lives+1)).destroy(); // CANNOT control player anymore~ cc.systemEvent.off(cc.SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this); cc.systemEvent.off(cc.SystemEvent.EventType.KEY_UP, this.onKeyUp, this); if(this.lives <= 0){ // cc.audioEngine.pauseMusic(); // cc.audioEngine.playEffect(this.gameoverSound,false); this.node.position = this.node.position; this.mainCamera.x = this.node.x; // Don't move!! // re gain lives /* firebase.database().ref('Users/'+user.displayName+'/').update({ Lives: 3 });*/ setTimeout(()=>{ // this.node.position = this.rebornPos; // this.node.getComponent(cc.RigidBody).enabled = true; // cc.director.loadScene("Gameover"); return; }, 4000); }else{ // Play die animation. // Stop bgm. cc.audioEngine.pauseMusic(); cc.audioEngine.playEffect(this.dieSound,false); // this.node.getComponent(cc.PhysicsBoxCollider).enabled = false; // Hide this node avoid multiple contact(DEAD). setTimeout(()=>{ cc.systemEvent.on(cc.SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this); cc.systemEvent.on(cc.SystemEvent.EventType.KEY_UP, this.onKeyUp, this); this.anim.play('idle'); this.node.position = this.rebornPos; this.node.getComponent(cc.PhysicsBoxCollider).enabled = true; cc.audioEngine.resumeMusic(); return; }, 2000); } }else{ // Still alive. if(this.LDown){ this.playerSpeed = -300; this.node.scaleX = -3.5; } else if(this.RDown){ this.playerSpeed = 300; this.node.scaleX = 3.5; }else{ // idle } this.node.x += this.playerSpeed * dt; if(this.SpaceDown && this.onGround){ this.jump(); } } } private jump() { // cc.log("Press Jump."); this.onGround = false; cc.audioEngine.playEffect(this.jumpSound,false); this.getComponent(cc.RigidBody).linearVelocity = cc.v2(0, 1500); } update(dt) { if(this.node.x < (this.mainCamera.x - this.Canvas.width/2) || this.node.x > (this.mainCamera.x + this.Canvas.width/2) ){ this.isDead = true; } this.playerMovement(dt); // this.mainCamera.x = this.node.x; this.coinText.string = this.coins.toString(); this.scoreText.string = this.score.toString(); this.rebornPos = cc.v2(this.Mario1.x+100 , this.Mario1.y); } // Callback functions for the beginning of contact. onBeginContact(contact, self, other) { // Mario on Top. if(contact.getWorldManifold().normal.y==-1){ if(other.node.name == "ground") { this.onGround = true; } else if(other.tag == 2) { // tag = 2: enemy. cc.log("Enemy die."); cc.audioEngine.playEffect(this.stompSound, false); if(other.node.name == "Enemy_Turtle"){ if(other.node.getComponent('Enemy_Turtle').beenhit == true){ this.onGround = true; // mario can jump. other.node.getComponent(cc.RigidBody).linearVelocity = cc.v2(0,0); } else{ this.getComponent(cc.RigidBody).linearVelocity = cc.v2(0,500); // Turtle would shrink in its TypeScript. } }else if(other.node.name =="Enemy_goomba"){ // goomba destroy in its TypeScript. }else{ other.node.destroy(); } } else if(other.tag == 3 || other.tag==4 || other.tag==7) { // any block or Qblock. this.onGround = true; } else if(other.node.name == "left_bound" || other.node.name == "right_bound" || other.node.name == "Lower_bound"){ this.isDead = true; }else if(other.node.name =="coin"){ cc.audioEngine.playEffect(this.coinSound,false); this.coins += 1; other.node.destroy(); }else if(other.tag == 5){ // grow up mushroom. cc.log("Eat mushroom."); if(!this.isBig){//can grow up // First update state to firebase. /* var user = firebase.auth().currentUser; firebase.database().ref('Users/'+user.displayName+'/').update({ isBig : 1 }).then(()=>{*/ this.isBig = true; this.growing = true; this.anim.play('mario_grow'); // this.playerAnimation('mario_grow'); // this.animateState = this.anim.play('mario_grow'); this.getComponent(cc.RigidBody).linearVelocity = cc.v2(0,200); this.changeSprite(this.BigMarioSprite); cc.audioEngine.playEffect(this.GrowSound, false); // }) }else{ cc.log("Already big!"); cc.audioEngine.playEffect(this.coinSound, false); } other.node.destroy(); // this.get bigger. // this.sprite.frame = ; }else if(other.node.name == "Lives"){ if(this.lives >=3){ this.lives = 3; //maintain. cc.log("Life saturated."); other.node.destroy(); } else{ this.lives ++; cc.log("Lives: " +this.lives); other.node.destroy(); //update to firebase /* var user = firebase.auth().currentUser; firebase.database().ref('Users/'+user.displayName+'/').update({ Lives: this.lives });*/ // Draw UI Life : var newlife = cc.instantiate(this.lifePrefab); this.lifeContainer.addChild(newlife); newlife.scaleX = 0.3; newlife.scaleY = 0.3; if(this.lives == 3){ newlife.name = "Life3" newlife.setPosition(50 +30*2 , 0); }else if(this.lives ==2){ newlife.name = "Life2"; newlife.setPosition(50 +30*1 , 0); }else{ newlife.name = "Life1"; newlife.setPosition(50 +30*0 , 0); } } } } // Mario at Bottom. else if(contact.getWorldManifold().normal.y== 1){ if(other.node.name == "ground") { } else if(other.tag == 2) { // tag = 2: enemy. cc.log("Enemy die."); // this.isDead = true; } else if(other.tag == 3 || other.tag==7) { }else if(other.tag == 4) { // Qblock. cc.log("Mario hits the question box."); switch(other.node.name){ case "Qblock0": if(!this.hitQblocks[0]){ this.hitQblocks[0] = true; let idx = this.randomGenItem(); let item = cc.instantiate(this.ItemPrefabs[idx]); // item : cc.Node this.Qblocks[0].addChild(item); item.setPosition(0,this.Qblocks[0].height); if(idx==0){ cc.audioEngine.playEffect(this.MushroomUp,false); }else if(idx==1){ cc.audioEngine.playEffect(this.coinSound,false); }else{ // Generate live. cc.audioEngine.playEffect(this.GrowSound,false); } }else{ cc.log(other.node.name + " has been hit."); } break; case "Qblock1": if(!this.hitQblocks[1]){ this.hitQblocks[1] = true; let idx = this.randomGenItem(); let item = cc.instantiate(this.ItemPrefabs[idx]); // item : cc.Node this.Qblocks[1].addChild(item); item.setPosition(0,this.Qblocks[1].height); if(idx==0){ cc.audioEngine.playEffect(this.MushroomUp,false); }else if(idx==1){ cc.audioEngine.playEffect(this.coinSound,false); }else{ // Generate live. cc.audioEngine.playEffect(this.GrowSound,false); } }else{ cc.log(other.node.name + " has been hit."); } break; case "Qblock2": if(!this.hitQblocks[2]){ this.hitQblocks[2] = true; let idx = this.randomGenItem(); let item = cc.instantiate(this.ItemPrefabs[idx]); // item : cc.Node this.Qblocks[2].addChild(item); item.setPosition(0,this.Qblocks[2].height); if(idx==0){ cc.audioEngine.playEffect(this.MushroomUp,false); }else if(idx==1){ cc.audioEngine.playEffect(this.coinSound,false); }else{ // Generate live. cc.audioEngine.playEffect(this.GrowSound,false); } }else{ cc.log(other.node.name + " has been hit."); } break; case "Qblock3": if(!this.hitQblocks[3]){ this.hitQblocks[3] = true; let idx = this.randomGenItem(); let item = cc.instantiate(this.ItemPrefabs[idx]); // item : cc.Node this.Qblocks[3].addChild(item); item.setPosition(0,this.Qblocks[3].height); if(idx==0){ cc.audioEngine.playEffect(this.MushroomUp,false); }else if(idx==1){ cc.audioEngine.playEffect(this.coinSound,false); }else{ // Generate live. cc.audioEngine.playEffect(this.GrowSound,false); } }else{ cc.log(other.node.name + " has been hit."); } break; case "Qblock4": if(!this.hitQblocks[4]){ this.hitQblocks[4] = true; let idx = this.randomGenItem(); let item = cc.instantiate(this.ItemPrefabs[idx]); // item : cc.Node this.Qblocks[4].addChild(item); item.setPosition(0,this.Qblocks[4].height); if(idx==0){ cc.audioEngine.playEffect(this.MushroomUp,false); }else if(idx==1){ cc.audioEngine.playEffect(this.coinSound,false); }else{ // Generate live. cc.audioEngine.playEffect(this.GrowSound,false); } }else{ cc.log(other.node.name + " has been hit."); } break; case "Qblock5": if(!this.hitQblocks[5]){ this.hitQblocks[5] = true; let idx = this.randomGenItem(); let item = cc.instantiate(this.ItemPrefabs[idx]); // item : cc.Node this.Qblocks[5].addChild(item); item.setPosition(0,this.Qblocks[5].height); if(idx==0){ cc.audioEngine.playEffect(this.MushroomUp,false); }else if(idx==1){ cc.audioEngine.playEffect(this.coinSound,false); }else{ // Generate live. cc.audioEngine.playEffect(this.GrowSound,false); } }else{ cc.log(other.node.name + " has been hit."); } break; case "Qblock6": if(!this.hitQblocks[6]){ this.hitQblocks[6] = true; let idx = this.randomGenItem(); let item = cc.instantiate(this.ItemPrefabs[idx]); // item : cc.Node this.Qblocks[6].addChild(item); item.setPosition(0,this.Qblocks[6].height); if(idx==0){ cc.audioEngine.playEffect(this.MushroomUp,false); }else if(idx==1){ cc.audioEngine.playEffect(this.coinSound,false); }else{ // Generate live. cc.audioEngine.playEffect(this.GrowSound,false); } }else{ cc.log(other.node.name + " has been hit."); } break; case "Qblock7": if(!this.hitQblocks[7]){ this.hitQblocks[7] = true; let idx = this.randomGenItem(); let item = cc.instantiate(this.ItemPrefabs[idx]); // item : cc.Node this.Qblocks[7].addChild(item); item.setPosition(0,this.Qblocks[7].height); if(idx==0){ cc.audioEngine.playEffect(this.MushroomUp,false); }else if(idx==1){ cc.audioEngine.playEffect(this.coinSound,false); }else{ // Generate live. cc.audioEngine.playEffect(this.GrowSound,false); } }else{ cc.log(other.node.name + " has been hit."); } break; } }else if(other.node.name =="coin"){ this.coins += 1; cc.audioEngine.playEffect(this.coinSound,false); other.node.destroy(); }else if(other.tag == 5){ // grow up mushroom. cc.log("Eat mushroom."); if(!this.isBig){//can grow up // First update state to firebase. /* var user = firebase.auth().currentUser; firebase.database().ref('Users/'+user.displayName+'/').update({ isBig : 1 }).then(()=>{*/ this.isBig = true; this.growing = true; this.anim.play('mario_grow'); // this.playerAnimation('mario_grow'); this.getComponent(cc.RigidBody).linearVelocity = cc.v2(0,200); this.changeSprite(this.BigMarioSprite); cc.audioEngine.playEffect(this.GrowSound, false); // }) // this.animateState = this.anim.play('mario_grow'); // this.isBig = true; // this.getComponent(cc.RigidBody).linearVelocity = cc.v2(0,200); // this.changeSprite(this.BigMarioSprite); // cc.audioEngine.playEffect(this.GrowSound, false); }else{ cc.log("Already big!"); cc.audioEngine.playEffect(this.coinSound, false); } other.node.destroy(); // this.get bigger. // this.sprite.frame = ; }else if(other.node.name == "Lives"){ if(this.lives >=3){ this.lives = 3; //maintain. cc.log("Life saturated."); other.node.destroy(); } else{ this.lives ++; cc.log("Lives: " +this.lives); other.node.destroy(); //update to firebase /* var user = firebase.auth().currentUser; firebase.database().ref('Users/'+user.displayName+'/').update({ Lives: this.lives });*/ // Draw UI Life : var newlife = cc.instantiate(this.lifePrefab); this.lifeContainer.addChild(newlife); newlife.scaleX = 0.3; newlife.scaleY = 0.3; if(this.lives == 3){ newlife.name = "Life3" newlife.setPosition(50 +30*2 , 0); }else if(this.lives ==2){ newlife.name = "Life2"; newlife.setPosition(50 +30*1 , 0); }else{ newlife.name = "Life1"; newlife.setPosition(50 +30*0 , 0); } } } } // Left or Right direction. else{ if(other.node.name == "ground") { this.onGround = true; } else if(other.tag == 2) { // tag = 2: enemy. if(other.node.name == "Enemy_Turtle"){ if(other.node.getComponent('Enemy_Turtle').beenhit == true){ cc.audioEngine.playEffect(this.kickSound,false); } else{ if(this.isBig){//Should shrink (won't die) // First update state to firebase. /* var user = firebase.auth().currentUser; firebase.database().ref('Users/'+user.displayName+'/').update({ isBig : 0 }).then(()=>{*/ this.isBig = false; this.shrinking = true; this.anim.play('mario_shrink'); // this.playerAnimation('mario_shrink'); this.changeSprite(this.smallMarioSprite); cc.audioEngine.playEffect(this.hurtSound, false); // }) }else{ cc.audioEngine.playEffect(this.hurtSound,false); this.isDead = true; } } } else{ if(this.isBig){//Should shrink (won't die) // First update state to firebase. /* var user = firebase.auth().currentUser; firebase.database().ref('Users/'+user.displayName+'/').update({ isBig : 0 }).then(()=>{*/ this.isBig = false; this.shrinking = true; this.anim.play('mario_shrink'); // this.playerAnimation('mario_shrink'); this.changeSprite(this.smallMarioSprite); cc.audioEngine.playEffect(this.hurtSound, false); // }) }else{ this.isDead = true; } } } else if(other.tag == 3 || other.tag==4) { this.onGround = true; } else if(other.tag == 5){ // grow up mushroom. cc.log("Eat mushroom."); if(!this.isBig){//can grow up // First update state to firebase./ /* var user = firebase.auth().currentUser; firebase.database().ref('Users/'+user.displayName+'/').update({ isBig : 1 }).then(()=>{*/ this.isBig = true; this.growing = true; this.anim.play('mario_grow'); // this.playerAnimation('mario_grow'); this.getComponent(cc.RigidBody).linearVelocity = cc.v2(0,200); this.changeSprite(this.BigMarioSprite); cc.audioEngine.playEffect(this.GrowSound, false); // }) }else{ cc.log("Already big!"); cc.audioEngine.playEffect(this.coinSound, false); } other.node.destroy(); // this.get bigger. // this.sprite.frame = ; } else if(other.node.name == "left_bound" || other.node.name == "right_bound" || other.node.name == "Lower_bound"){ this.isDead = true; } else if(other.tag == 6){//reach goal flag. cc.systemEvent.off(cc.SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this); cc.systemEvent.off(cc.SystemEvent.EventType.KEY_UP, this.onKeyUp, this); contact.disabled = true; this.LDown = this.RDown = this.SpaceDown = false; //this.playerSpeed = 0; let action = cc.repeatForever(cc.moveBy(1, 0, 1500)) cc.log("Run action now."); this.node.runAction(action); // this.playerAnimation('mario_win'); this.animateState = this.anim.play('mario_win'); // Send "win" to level_1.ts. let Rtime = cc.find("Canvas").getComponent("level_2").remainTime; this.score = Rtime.toFixed(0).toString().replace(".", ":"); cc.find("Canvas").getComponent("level_2").win(); }else if(other.node.name =="coin"){ this.coins += 1; cc.audioEngine.playEffect(this.coinSound,false); other.node.destroy(); }else if(other.node.name == "Lives"){ if(this.lives >=3){ this.lives = 3; //maintain. cc.log("Life saturated."); other.node.destroy(); } else{ this.lives ++; cc.log("Lives: " +this.lives); other.node.destroy(); //update to firebase /* var user = firebase.auth().currentUser; firebase.database().ref('Users/'+user.displayName+'/').update({ Lives: this.lives });*/ // Draw UI Life : var newlife = cc.instantiate(this.lifePrefab); this.lifeContainer.addChild(newlife); newlife.scaleX = 0.3; newlife.scaleY = 0.3; if(this.lives == 3){ newlife.name = "Life3" newlife.setPosition(50 +30*2 , 0); }else if(this.lives ==2){ newlife.name = "Life2"; newlife.setPosition(50 +30*1 , 0); }else{ newlife.name = "Life1"; newlife.setPosition(50 +30*0 , 0); } } } } } // End onBeginContact. private playerAnimation(anim_str: string) { var playing = this.anim.play(this.anim_str).isPlaying; // Grow, shrink, die need to be frozen. CANNOT change to idle if(anim_str == this.anim_str)return; else{ cc.log("Change amim!"); cc.log("Now: "+this.anim_str+", Play: "+anim_str); this.anim.stop(); this.anim_str = anim_str; this.anim.play(this.anim_str); } } randomGenItem() { let rand = Math.random(); //0: Mushroom, 1: coin, 2: lives. let prob = [1,1,1]; let sum = prob.reduce((a,b)=>a+b); for(let i = 1; i < prob.length; i++) prob[i] += prob[i-1]; for(let i = 0; i < prob.length; i++) { prob[i] /= sum; if(rand <= prob[i]) return i; } } changeSprite(Sprite: cc.SpriteFrame) { // change sprite frame to the one specified by the property this.getComponent(cc.Sprite).spriteFrame = Sprite; } } ``` # Code Description : :::success 程式碼講解:我針對**3個**較重點的程式碼說明。 ::: ### 1. Player.ts 一切與玩家(mario)相關的code都寫在這裡面。從`onload()` `start()` `update(dt)` 三個核心**method**下去講解。 **(1)onload()**: 程式載入後做三件事情: **1.開啟PhysicsManager()** **2.取得user現在性命數量** (Lives) **3.取得user現在大小狀態** (isBig) ``` typescript= onLoad() { cc.director.getPhysicsManager().enabled = true;      // this.idleFrame = this.getComponent(cc.Sprite).spriteFrame; this.anim = this.getComponent(cc.Animation); // Read lives from user. var user = firebase.auth().currentUser; var LifeRef = firebase.database().ref('Users/'+user.displayName+'/Lives'); // 快照 LifeRef.once('value', (snapshot)=>{ console.log(snapshot.val()); this.lives = snapshot.val(); }).then( ()=>{ // Draw life prefabs for(let i=1; i<=this.lives; i++){ var newlife = cc.instantiate(this.lifePrefab); newlife.name = "Life"+String(i); cc.log(newlife.name); this.lifeContainer.addChild(newlife); newlife.scaleX = 0.3; newlife.scaleY = 0.3; newlife.setPosition(50 +30*(i-1) , 0); } }); ////// Read isBig ////////// firebase.database().ref('Users/'+user.displayName+'/isBig').once('value', (snapshot)=>{ console.log(snapshot.val()); if(snapshot.val()==1) this.isBig = true; else this.isBig = false; }).then( ()=>{ // Draw mario sprite Frame. if(this.isBig) this.changeSprite(this.BigMarioSprite); else this.changeSprite(this.smallMarioSprite); }); // this.lives = 3; //initiall lives. // Reset Q blocks to false. for(let k=0; k<=7; k++){ this.hitQblocks[k] = false; cc.log("Qblock "+k+" :"+this.hitQblocks[k]); } } ``` **(2)start()**: onLoad()後做四件事情: **1.開啟Keyboard Event listener** **2.取得UI的`cc.Label`元素,作為此class的Propteries** ``(coinText, scoreText)`` **3.初始化Player的動畫`idle`** **4.取得reborn位置 `rebornPos`** ``` typescript= start(){ cc.systemEvent.on(cc.SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this); cc.systemEvent.on(cc.SystemEvent.EventType.KEY_UP, this.onKeyUp, this); this.coinText = cc.find("Canvas/Main Camera/level1/TopContent/Coins/number").getComponent(cc.Label); this.scoreText = cc.find("Canvas/Main Camera/level1/TopContent/Score/value").getComponent(cc.Label); this.anim.play('idle'); this.rebornPos = this.node.position; } ``` **(3)update(dt)**: 每dt時間更新一次: **1.更新玩家位置`playerMovement(dt)`** **2.更新Camera位置`this.mainCamera.x`** **3.更新`coin`數量** **4.更新`score`數量** ``` typescript= update(dt) { this.playerMovement(dt); this.mainCamera.x = this.node.x; this.coinText.string = this.coins.toString(); this.scoreText.string = this.score.toString(); } ``` * 完整的Player.ts如下: ``` typescript= // Learn TypeScript: // - [Chinese] https://docs.cocos.com/creator/manual/zh/scripting/typescript.html // - [English] http://www.cocos2d-x.org/docs/creator/manual/en/scripting/typescript.html // Learn Attribute: // - [Chinese] https://docs.cocos.com/creator/manual/zh/scripting/reference/attributes.html // - [English] http://www.cocos2d-x.org/docs/creator/manual/en/scripting/reference/attributes.html // Learn life-cycle callbacks: // - [Chinese] https://docs.cocos.com/creator/manual/zh/scripting/life-cycle-callbacks.html // - [English] http://www.cocos2d-x.org/docs/creator/manual/en/scripting/life-cycle-callbacks.html // TODO : 1 : KEY.down : 蹲下(動畫?) import GameManager from "./GameManager"; import level_1 from "./level_1"; import Enemy_Turtle from "./Enemy_Turtle"; const {ccclass, property} = cc._decorator; @ccclass export default class Player extends cc.Component { @property(cc.SpriteFrame) BigMarioSprite: cc.SpriteFrame = null; @property(cc.SpriteFrame) smallMarioSprite: cc.SpriteFrame = null; @property({type:cc.AudioClip}) GrowSound: cc.AudioClip = null; @property({type:cc.AudioClip}) ShrinkSound: cc.AudioClip = null; @property({type:cc.AudioClip}) hurtSound: cc.AudioClip = null; @property({type:cc.AudioClip}) dieSound: cc.AudioClip = null; @property({type:cc.AudioClip}) jumpSound: cc.AudioClip = null; @property({type:cc.AudioClip}) kickSound: cc.AudioClip = null; @property({type:cc.AudioClip}) stompSound: cc.AudioClip = null; @property({type:cc.AudioClip}) MushroomUp: cc.AudioClip = null; @property({type:cc.AudioClip}) reserveSound: cc.AudioClip = null; @property({type:cc.AudioClip}) coinSound: cc.AudioClip = null; @property({type:cc.AudioClip}) gameoverSound: cc.AudioClip = null; @property(GameManager) gameMgr: GameManager = null; // @property(cc.Node) // level_1: cc.Node = null; @property({type: cc.Node}) mainCamera: cc.Node = null; @property({type: cc.Node}) Qblocks: cc.Node[] = []; private hitQblocks :boolean[] = [false,false,false,false,false,false,false,false]; @property([cc.Prefab]) ItemPrefabs: cc.Prefab[] = []; // 0:M 1:coin // private idleFrame: cc.SpriteFrame = null; // private anim: cc.Animation = null; private rebornPos = null; isBig: boolean = false; //defaule: small mario. lives: number = null; // private ceilingPos: number = 155; // private fallDown: boolean = false; private damageTime: number = 0; private growing: boolean = false; private shrinking: boolean = false; private dieing: boolean = false; @property(cc.Node) lifeContainer: cc.Node = null; @property(cc.Prefab) lifePrefab: cc.Prefab = null; coinText : cc.Label = null; scoreText : cc.Label = null; private anim = null; //this will use to get animation component private anim_str: string = null; private animateState = null; //this will use to record animationState // private bulletPool = null; // this is a bullet manager, and it control the bullet resource private playerSpeed: number = 0; // LIFE-CYCLE CALLBACKS: // @property(cc.Prefab) // private bulletPrefab: cc.Prefab = null; private LDown: boolean = false; // key for player to go left private RDown: boolean = false; // key for player to go right // private jDown: boolean = false; // key for player to shoot private SpaceDown: boolean = false; // key for player to jump private isDead: boolean = false; private onGround: boolean = false; score : number = 0; coins : number = 0; onLoad() { cc.director.getPhysicsManager().enabled = true;      // this.idleFrame = this.getComponent(cc.Sprite).spriteFrame; this.anim = this.getComponent(cc.Animation); // Read lives from user. var user = firebase.auth().currentUser; var LifeRef = firebase.database().ref('Users/'+user.displayName+'/Lives'); // 快照 LifeRef.once('value', (snapshot)=>{ console.log(snapshot.val()); this.lives = snapshot.val(); }).then( ()=>{ // Draw life prefabs for(let i=1; i<=this.lives; i++){ var newlife = cc.instantiate(this.lifePrefab); newlife.name = "Life"+String(i); cc.log(newlife.name); this.lifeContainer.addChild(newlife); newlife.scaleX = 0.3; newlife.scaleY = 0.3; newlife.setPosition(50 +30*(i-1) , 0); } }); ////// Read isBig ////////// firebase.database().ref('Users/'+user.displayName+'/isBig').once('value', (snapshot)=>{ console.log(snapshot.val()); if(snapshot.val()==1) this.isBig = true; else this.isBig = false; }).then( ()=>{ // Draw mario sprite Frame. if(this.isBig) this.changeSprite(this.BigMarioSprite); else this.changeSprite(this.smallMarioSprite); }); // this.lives = 3; //initiall lives. // Reset Q blocks to false. for(let k=0; k<=7; k++){ this.hitQblocks[k] = false; cc.log("Qblock "+k+" :"+this.hitQblocks[k]); } } start(){ cc.systemEvent.on(cc.SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this); cc.systemEvent.on(cc.SystemEvent.EventType.KEY_UP, this.onKeyUp, this); this.coinText = cc.find("Canvas/Main Camera/level1/TopContent/Coins/number").getComponent(cc.Label); this.scoreText = cc.find("Canvas/Main Camera/level1/TopContent/Score/value").getComponent(cc.Label); // this.animateState = this.anim.play('idle'); // Initial animation this.anim.play('idle'); this.rebornPos = this.node.position; } onKeyDown(event) { // cc.log("Key Down: " + event.keyCode); if(event.keyCode == cc.macro.KEY.left) { if(this.isBig)this.anim.play('Big_move'); else this.anim.play('move'); this.LDown = true; this.RDown = false; } else if(event.keyCode == cc.macro.KEY.right) { if(this.isBig)this.anim.play('Big_move'); else this.anim.play('move'); this.RDown = true; this.LDown = false; } else if(event.keyCode == cc.macro.KEY.space) { ///////// TODO : BIG-JUMP animation. ////////// if(this.isBig)this.anim.play('Big_jump'); else this.anim.play('mario_jump'); this.SpaceDown = true; } } onKeyUp(event) { if(event.keyCode == cc.macro.KEY.left){ // this.animateState = this.anim.stop('move'); if(this.isBig) this.anim.play('Big_idle'); else this.anim.play('idle'); this.LDown = false; } else if(event.keyCode == cc.macro.KEY.right){ if(this.isBig) this.anim.play('Big_idle'); else this.anim.play('idle'); this.RDown = false; } else if(event.keyCode == cc.macro.KEY.space) // if(this.isBig) this.anim.play('Big_idle'); // else this.anim.play('idle'); this.SpaceDown = false; } private playerMovement(dt) { this.playerSpeed = 0; if(this.isDead) { this.LDown = false; this.RDown = false; this.SpaceDown = false; this.lives -- ; let user = firebase.auth().currentUser; firebase.database().ref('Users/'+user.displayName+'/').update({ Lives: this.lives }); this.isDead = false; this.getComponent(cc.RigidBody).linearVelocity = cc.v2(0, 1500); this.node.getComponent(cc.PhysicsBoxCollider).enabled = false; // Hide this node avoid multiple contact(DEAD). // this.playerAnimation('mario_die'); this.anim.play('mario_die'); this.lifeContainer.getChildByName("Life"+String(this.lives+1)).destroy(); // CANNOT control player anymore~ cc.systemEvent.off(cc.SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this); cc.systemEvent.off(cc.SystemEvent.EventType.KEY_UP, this.onKeyUp, this); if(this.lives <= 0){ cc.audioEngine.pauseMusic(); cc.audioEngine.playEffect(this.gameoverSound,false); this.node.position = this.node.position; this.mainCamera.x = this.node.x; // Don't move!! // re gain lives firebase.database().ref('Users/'+user.displayName+'/').update({ Lives: 3, isBig: 0 // return to small mario. }) setTimeout(()=>{ this.node.position = this.rebornPos; // this.node.getComponent(cc.RigidBody).enabled = true; cc.director.loadScene("Gameover"); return; }, 4000); }else{ // Play die animation. // Stop bgm. cc.audioEngine.pauseMusic(); cc.audioEngine.playEffect(this.dieSound,false); // this.node.getComponent(cc.PhysicsBoxCollider).enabled = false; // Hide this node avoid multiple contact(DEAD). setTimeout(()=>{ cc.systemEvent.on(cc.SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this); cc.systemEvent.on(cc.SystemEvent.EventType.KEY_UP, this.onKeyUp, this); this.anim.play('idle'); this.node.position = this.rebornPos; this.node.getComponent(cc.PhysicsBoxCollider).enabled = true; cc.audioEngine.resumeMusic(); return; }, 2000); } }else{ // Still alive. if(this.LDown){ this.playerSpeed = -300; this.node.scaleX = -3.5; } else if(this.RDown){ this.playerSpeed = 300; this.node.scaleX = 3.5; }else{ // idle } this.node.x += this.playerSpeed * dt; if(this.SpaceDown && this.onGround){ this.jump(); } } } private jump() { // cc.log("Press Jump."); this.onGround = false; cc.audioEngine.playEffect(this.jumpSound,false); this.getComponent(cc.RigidBody).linearVelocity = cc.v2(0, 1500); } update(dt) { this.playerMovement(dt); this.mainCamera.x = this.node.x; this.coinText.string = this.coins.toString(); this.scoreText.string = this.score.toString(); } // Callback functions for the beginning of contact. onBeginContact(contact, self, other) { // Mario on Top. if(contact.getWorldManifold().normal.y==-1){ if(other.node.name == "ground") { this.onGround = true; } else if(other.tag == 2) { // tag = 2: enemy. // cc.log("Enemy die."); cc.audioEngine.playEffect(this.stompSound, false); if(other.node.name == "Enemy_Turtle"){ if(other.node.getComponent('Enemy_Turtle').beenhit == true){ this.onGround = true; // mario can jump. other.node.getComponent(cc.RigidBody).linearVelocity = cc.v2(0,0); } else{ this.getComponent(cc.RigidBody).linearVelocity = cc.v2(0,500); // Turtle would shrink in its TypeScript. } }else if(other.node.name =="Enemy_goomba"){ // goomba destroy in its TypeScript. }else{ other.node.destroy(); } } else if(other.tag == 3 || other.tag==4 || other.tag==7) { // any block or Qblock. this.onGround = true; } else if(other.node.name == "left_bound" || other.node.name == "right_bound" || other.node.name == "Lower_bound"){ this.isDead = true; }else if(other.node.name =="coin"){ cc.audioEngine.playEffect(this.coinSound,false); this.coins += 1; other.node.destroy(); }else if(other.tag == 5){ // grow up mushroom. cc.log("Eat mushroom."); if(!this.isBig){//can grow up // First update state to firebase. var user = firebase.auth().currentUser; firebase.database().ref('Users/'+user.displayName+'/').update({ isBig : 1 }).then(()=>{ this.isBig = true; this.growing = true; this.anim.play('mario_grow'); // this.playerAnimation('mario_grow'); // this.animateState = this.anim.play('mario_grow'); this.getComponent(cc.RigidBody).linearVelocity = cc.v2(0,200); this.changeSprite(this.BigMarioSprite); cc.audioEngine.playEffect(this.GrowSound, false); }) }else{ cc.log("Already big!"); cc.audioEngine.playEffect(this.coinSound, false); } other.node.destroy(); // this.get bigger. // this.sprite.frame = ; }else if(other.node.name == "Lives_item"){ cc.audioEngine.playEffect(this.reserveSound, false); if(this.lives >=3){ this.lives = 3; //maintain. cc.log("Life saturated."); other.node.destroy(); } else{ this.lives ++; cc.log("Lives: " +this.lives); other.node.destroy(); //update to firebase var user = firebase.auth().currentUser; firebase.database().ref('Users/'+user.displayName+'/').update({ Lives: this.lives }); // Draw UI Life : var newlife = cc.instantiate(this.lifePrefab); this.lifeContainer.addChild(newlife); newlife.scaleX = 0.3; newlife.scaleY = 0.3; if(this.lives == 3){ newlife.name = "Life3" newlife.setPosition(50 +30*2 , 0); }else if(this.lives ==2){ newlife.name = "Life2"; newlife.setPosition(50 +30*1 , 0); }else{ newlife.name = "Life1"; newlife.setPosition(50 +30*0 , 0); } } } } // Mario at Bottom. else if(contact.getWorldManifold().normal.y== 1){ if(other.node.name == "ground") { } else if(other.tag == 2) { // tag = 2: enemy. // mario die. } else if(other.tag == 3 || other.tag==7) { }else if(other.tag == 4) { // Qblock. cc.log("Mario hits the question box."); switch(other.node.name){ case "Qblock0": if(!this.hitQblocks[0]){ this.hitQblocks[0] = true; let idx = this.randomGenItem(); let item = cc.instantiate(this.ItemPrefabs[idx]); // item : cc.Node this.Qblocks[0].addChild(item); item.setPosition(0,this.Qblocks[0].height); if(idx==0){ cc.audioEngine.playEffect(this.MushroomUp,false); }else if(idx==1){ cc.audioEngine.playEffect(this.coinSound,false); }else{ // Generate live. cc.audioEngine.playEffect(this.GrowSound,false); } }else{ cc.log(other.node.name + " has been hit."); } break; case "Qblock1": if(!this.hitQblocks[1]){ this.hitQblocks[1] = true; let idx = this.randomGenItem(); let item = cc.instantiate(this.ItemPrefabs[idx]); // item : cc.Node this.Qblocks[1].addChild(item); item.setPosition(0,this.Qblocks[1].height); if(idx==0){ cc.audioEngine.playEffect(this.MushroomUp,false); }else if(idx==1){ cc.audioEngine.playEffect(this.coinSound,false); }else{ // Generate live. cc.audioEngine.playEffect(this.GrowSound,false); } }else{ cc.log(other.node.name + " has been hit."); } break; case "Qblock2": if(!this.hitQblocks[2]){ this.hitQblocks[2] = true; let idx = this.randomGenItem(); let item = cc.instantiate(this.ItemPrefabs[idx]); // item : cc.Node this.Qblocks[2].addChild(item); item.setPosition(0,this.Qblocks[2].height); if(idx==0){ cc.audioEngine.playEffect(this.MushroomUp,false); }else if(idx==1){ cc.audioEngine.playEffect(this.coinSound,false); }else{ // Generate live. cc.audioEngine.playEffect(this.GrowSound,false); } }else{ cc.log(other.node.name + " has been hit."); } break; case "Qblock3": if(!this.hitQblocks[3]){ this.hitQblocks[3] = true; let idx = this.randomGenItem(); let item = cc.instantiate(this.ItemPrefabs[idx]); // item : cc.Node this.Qblocks[3].addChild(item); item.setPosition(0,this.Qblocks[3].height); if(idx==0){ cc.audioEngine.playEffect(this.MushroomUp,false); }else if(idx==1){ cc.audioEngine.playEffect(this.coinSound,false); }else{ // Generate live. cc.audioEngine.playEffect(this.GrowSound,false); } }else{ cc.log(other.node.name + " has been hit."); } break; case "Qblock4": if(!this.hitQblocks[4]){ this.hitQblocks[4] = true; let idx = this.randomGenItem(); let item = cc.instantiate(this.ItemPrefabs[idx]); // item : cc.Node this.Qblocks[4].addChild(item); item.setPosition(0,this.Qblocks[4].height); if(idx==0){ cc.audioEngine.playEffect(this.MushroomUp,false); }else if(idx==1){ cc.audioEngine.playEffect(this.coinSound,false); }else{ // Generate live. cc.audioEngine.playEffect(this.GrowSound,false); } }else{ cc.log(other.node.name + " has been hit."); } break; case "Qblock5": if(!this.hitQblocks[5]){ this.hitQblocks[5] = true; let idx = this.randomGenItem(); let item = cc.instantiate(this.ItemPrefabs[idx]); // item : cc.Node this.Qblocks[5].addChild(item); item.setPosition(0,this.Qblocks[5].height); if(idx==0){ cc.audioEngine.playEffect(this.MushroomUp,false); }else if(idx==1){ cc.audioEngine.playEffect(this.coinSound,false); }else{ // Generate live. cc.audioEngine.playEffect(this.GrowSound,false); } }else{ cc.log(other.node.name + " has been hit."); } break; case "Qblock6": if(!this.hitQblocks[6]){ this.hitQblocks[6] = true; let idx = this.randomGenItem(); let item = cc.instantiate(this.ItemPrefabs[idx]); // item : cc.Node this.Qblocks[6].addChild(item); item.setPosition(0,this.Qblocks[6].height); if(idx==0){ cc.audioEngine.playEffect(this.MushroomUp,false); }else if(idx==1){ cc.audioEngine.playEffect(this.coinSound,false); }else{ // Generate live. cc.audioEngine.playEffect(this.GrowSound,false); } }else{ cc.log(other.node.name + " has been hit."); } break; case "Qblock7": if(!this.hitQblocks[7]){ this.hitQblocks[7] = true; let idx = this.randomGenItem(); let item = cc.instantiate(this.ItemPrefabs[idx]); // item : cc.Node this.Qblocks[7].addChild(item); item.setPosition(0,this.Qblocks[7].height); if(idx==0){ cc.audioEngine.playEffect(this.MushroomUp,false); }else if(idx==1){ cc.audioEngine.playEffect(this.coinSound,false); }else{ // Generate live. cc.audioEngine.playEffect(this.GrowSound,false); } }else{ cc.log(other.node.name + " has been hit."); } break; } }else if(other.node.name =="coin"){ this.coins += 1; cc.audioEngine.playEffect(this.coinSound,false); other.node.destroy(); }else if(other.tag == 5){ // grow up mushroom. cc.log("Eat mushroom."); if(!this.isBig){//can grow up // First update state to firebase. var user = firebase.auth().currentUser; firebase.database().ref('Users/'+user.displayName+'/').update({ isBig : 1 }).then(()=>{ this.isBig = true; this.growing = true; this.anim.play('mario_grow'); // this.playerAnimation('mario_grow'); this.getComponent(cc.RigidBody).linearVelocity = cc.v2(0,200); this.changeSprite(this.BigMarioSprite); cc.audioEngine.playEffect(this.GrowSound, false); }) // this.animateState = this.anim.play('mario_grow'); // this.isBig = true; // this.getComponent(cc.RigidBody).linearVelocity = cc.v2(0,200); // this.changeSprite(this.BigMarioSprite); // cc.audioEngine.playEffect(this.GrowSound, false); }else{ cc.log("Already big!"); cc.audioEngine.playEffect(this.coinSound, false); } other.node.destroy(); // this.get bigger. // this.sprite.frame = ; }else if(other.node.name == "Lives_item"){ cc.audioEngine.playEffect(this.reserveSound, false); if(this.lives >=3){ this.lives = 3; //maintain. cc.log("Life saturated."); other.node.destroy(); } else{ this.lives ++; cc.log("Lives: " +this.lives); other.node.destroy(); //update to firebase var user = firebase.auth().currentUser; firebase.database().ref('Users/'+user.displayName+'/').update({ Lives: this.lives }); // Draw UI Life : var newlife = cc.instantiate(this.lifePrefab); this.lifeContainer.addChild(newlife); newlife.scaleX = 0.3; newlife.scaleY = 0.3; if(this.lives == 3){ newlife.name = "Life3" newlife.setPosition(50 +30*2 , 0); }else if(this.lives ==2){ newlife.name = "Life2"; newlife.setPosition(50 +30*1 , 0); }else{ newlife.name = "Life1"; newlife.setPosition(50 +30*0 , 0); } } } } // Left or Right direction. else{ if(other.node.name == "ground") { this.onGround = true; } else if(other.tag == 2) { // tag = 2: enemy. if(other.node.name == "Enemy_Turtle"){ if(other.node.getComponent('Enemy_Turtle').beenhit == true){ cc.audioEngine.playEffect(this.kickSound,false); } else{ if(this.isBig){//Should shrink (won't die) this.shrinking = true; cc.log("shrink!"); // First update state to firebase. var user = firebase.auth().currentUser; firebase.database().ref('Users/'+user.displayName+'/').update({ isBig : 0 }).then(()=>{ this.isBig = false; // this.shrinking = true; this.anim.play('mario_shrink'); // this.playerAnimation('mario_shrink'); this.changeSprite(this.smallMarioSprite); cc.audioEngine.playEffect(this.hurtSound, false); }) }else{ if(this.shrinking){ cc.log("Now shrinking..."); // One second unhurtable time. this.scheduleOnce( ()=>{ this.shrinking = false; }, 1) }else{ this.isDead = true; } } } } else{ // goomba or flower. if(this.isBig){//Should shrink (won't die) this.shrinking = true // First update state to firebase. var user = firebase.auth().currentUser; firebase.database().ref('Users/'+user.displayName+'/').update({ isBig : 0 }).then(()=>{ this.isBig = false; // this.shrinking = true; this.anim.play('mario_shrink'); this.changeSprite(this.smallMarioSprite); cc.audioEngine.playEffect(this.hurtSound, false); }) }else{ if(this.shrinking){ cc.log("Now shrinking..."); // One second unhurtable time. this.scheduleOnce( ()=>{ this.shrinking = false; }, 1) }else{ this.isDead = true; } } } } else if(other.tag == 3 || other.tag==4) { this.onGround = true; } else if(other.tag == 5){ // grow up mushroom. cc.log("Eat mushroom."); if(!this.isBig){//can grow up // First update state to firebase. var user = firebase.auth().currentUser; firebase.database().ref('Users/'+user.displayName+'/').update({ isBig : 1 }).then(()=>{ this.isBig = true; this.growing = true; this.anim.play('mario_grow'); // this.playerAnimation('mario_grow'); this.getComponent(cc.RigidBody).linearVelocity = cc.v2(0,200); this.changeSprite(this.BigMarioSprite); cc.audioEngine.playEffect(this.GrowSound, false); }) }else{ cc.log("Already big!"); cc.audioEngine.playEffect(this.coinSound, false); } other.node.destroy(); // this.get bigger. // this.sprite.frame = ; } else if(other.node.name == "left_bound" || other.node.name == "right_bound" || other.node.name == "Lower_bound"){ this.isDead = true; } else if(other.tag == 6){//reach goal flag. cc.systemEvent.off(cc.SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this); cc.systemEvent.off(cc.SystemEvent.EventType.KEY_UP, this.onKeyUp, this); contact.disabled = true; this.LDown = this.RDown = this.SpaceDown = false; //this.playerSpeed = 0; let action = cc.repeatForever(cc.moveBy(1, 0, 1500)) cc.log("Run action now."); this.node.runAction(action); // this.playerAnimation('mario_win'); if(this.isBig)this.animateState = this.anim.play('Big_win'); else this.animateState = this.anim.play('mario_win'); // Send "win" to level_1.ts. let Rtime = cc.find("Canvas").getComponent("level_1").remainTime; this.score = Rtime.toFixed(0).toString().replace(".", ":"); cc.find("Canvas").getComponent("level_1").win(); }else if(other.node.name =="coin"){ this.coins += 1; cc.audioEngine.playEffect(this.coinSound,false); other.node.destroy(); }else if(other.node.name == "Lives_item"){ cc.audioEngine.playEffect(this.reserveSound, false); cc.log("Gain_lives!"); if(this.lives >=3){ this.lives = 3; //maintain. cc.log("Life saturated."); other.node.destroy(); } else{ this.lives ++; cc.log("Lives: " +this.lives); other.node.destroy(); //update to firebase var user = firebase.auth().currentUser; firebase.database().ref('Users/'+user.displayName+'/').update({ Lives: this.lives }); // Draw UI Life : var newlife = cc.instantiate(this.lifePrefab); this.lifeContainer.addChild(newlife); newlife.scaleX = 0.3; newlife.scaleY = 0.3; if(this.lives == 3){ newlife.name = "Life3" newlife.setPosition(50 +30*2 , 0); }else if(this.lives ==2){ newlife.name = "Life2"; newlife.setPosition(50 +30*1 , 0); }else{ newlife.name = "Life1"; newlife.setPosition(50 +30*0 , 0); } } } } } // End onBeginContact. private playerAnimation(anim_str: string) { var playing = this.anim.play(this.anim_str).isPlaying; // Grow, shrink, die need to be frozen. CANNOT change to idle if(anim_str == this.anim_str)return; else{ cc.log("Change amim!"); cc.log("Now: "+this.anim_str+", Play: "+anim_str); this.anim.stop(); this.anim_str = anim_str; this.anim.play(this.anim_str); } } randomGenItem() { let rand = Math.random(); //0: Mushroom, 1: coin, 2: lives. let prob = [1,1,1]; let sum = prob.reduce((a,b)=>a+b); for(let i = 1; i < prob.length; i++) prob[i] += prob[i-1]; for(let i = 0; i < prob.length; i++) { prob[i] /= sum; if(rand <= prob[i]) return i; } } changeSprite(Sprite: cc.SpriteFrame) { // change sprite frame to the one specified by the property this.getComponent(cc.Sprite).spriteFrame = Sprite; } } ``` ### 2. level_1.ts 控制level_1的遊戲程序的主要程式。從`onload()` `start()` `update(dt)` 三個核心**method**下去講解。 **(1)onload()**: 程式載入後做兩件事情: **1.初始化class properties** **2.初始化`this.gameover`為 *false*** ``` typescript= onLoad () { this.initProperties(); this.gameover = false; } ``` 其中`initPropterties()`: ``` typescript= initProperties(){ this.levelText = cc.find("Canvas/Main Camera/level1/TopContent/Level_one/level").getComponent(cc.Label); this.timeText = cc.find("Canvas/Main Camera/level1/TopContent/Time/time").getComponent(cc.Label); } ``` **(2)start()**: onLoad()後做兩件事情: **1.撥放背景音樂** **2.初始化Pause button `this.Toggle_init()`** ``` typescript= start () { this.playBGM(); this.Toggle_init(); } ``` **(3)update(dt)**: 每dt時間更新一次: **1.若遊戲還在繼續 `if(!this.gameover)`** **2.更新UI參數(`Time`,`level`)** **3.如果時間倒數到0,遊戲結束** ``` typescript= update (dt) { if(!this.gameover){ this.updateUI(dt); if(this.remainTime == 0){ this.gameover = true; this.onGameover(); } } } ``` `updateUI(dt):` ``` typescript= updateUI(dt){ this.levelText.string = this.gameLevel.toString(); if(!this.Win){ this.remainTime -= dt; if(this.remainTime < 0){ this.remainTime = 0; } } this.timeText.string = this.remainTime.toFixed(0).toString().replace(".", ":"); } ``` :::danger **最重要的`win()` Property:** ``` typescript= win(){ cc.log("win"); this.stopBGM(); // stop timer. this.Win = true; cc.audioEngine.playMusic(this.victory, false); var user = firebase.auth().currentUser; // num_of_times ++; firebase.database().ref('Users/'+user.displayName+'/num_of_times').once('value',(snapshot)=>{ // get current time, then increment it by one. firebase.database().ref('Users/'+user.displayName).update({ num_of_times: snapshot.val()+ 1 }).then(()=>{ // Log score data. firebase.database().ref('Users/'+user.displayName+'/num_of_times').once('value',(snapshot)=>{ // get current times. firebase.database().ref('Users/'+user.displayName+'/'+snapshot.val()).set({ scores : Number(cc.find("Canvas/mario").getComponent("Player").score), time: Number(300- Number(this.remainTime.toFixed(0))) }) }) }) }) // Check if need to update 'Best_score' firebase.database().ref('Users/'+user.displayName+'/Best_score').once('value',(snapshot)=>{ // get current times. if(cc.find("Canvas/mario").getComponent("Player").score.toString() > snapshot.val()){ // Update Best_score. firebase.database().ref('Users/'+user.displayName+'/').update({ Best_score: cc.find("Canvas/mario").getComponent("Player").score.toString() }) } }) // Update user lives. firebase.database().ref('Users/'+user.displayName+'/').update({ Lives: cc.find("Canvas/mario").getComponent("Player").lives }) setTimeout(()=>{ cc.director.loadScene("1to2"); return; }, 5000); } ``` ::: * 另外在遊戲**暫停**部分,code部分如下 ``` typescript= Pause(event, customEventData) { // 这里的 toggle 是事件发出的 Toggle 组件 // 这里的 customEventData 参数就等于之前设置的 "foobar" // Pause. // Turn off button listener. // this.OFFPause(); this.count++; if(this.count==1){ // Draw UI Life : var pause = cc.instantiate(this.PauseBoard); this.Canvas.addChild(pause); pause.setPosition(this.MainCamera.x, this.MainCamera.y); // Init resume btns. this.resume_init(); cc.log("Pause"); this.stopBGM(); cc.director.pause(); }else{ cc.log("Cannot press Pause since Now pausing."); } } ``` * 然後暫停後的**恢復遊戲**,**離開遊戲**如下: ``` typescript= Resume(event, customEventData){ // Destroy pause board and resume the game! this.count = 0; cc.log("Resume"); cc.find("Canvas/pause").destroy(); cc.director.resume(); this.resumeBGM(); } Quit(event, customEventData){ // Quit game this.count = 0; cc.find("Canvas/pause").destroy(); cc.director.loadScene("setting"); } ``` * 完整的level_1.ts如下: ``` typescript= // Learn TypeScript: // - [Chinese] https://docs.cocos.com/creator/manual/zh/scripting/typescript.html // - [English] http://www.cocos2d-x.org/docs/creator/manual/en/scripting/typescript.html // Learn Attribute: // - [Chinese] https://docs.cocos.com/creator/manual/zh/scripting/reference/attributes.html // - [English] http://www.cocos2d-x.org/docs/creator/manual/en/scripting/reference/attributes.html // Learn life-cycle callbacks: // - [Chinese] https://docs.cocos.com/creator/manual/zh/scripting/life-cycle-callbacks.html // - [English] http://www.cocos2d-x.org/docs/creator/manual/en/scripting/life-cycle-callbacks.html import Player from "./Player"; const {ccclass, property} = cc._decorator; @ccclass export default class level_1 extends cc.Component { @property({type:cc.AudioClip}) bgm: cc.AudioClip = null; @property({type:cc.AudioClip}) touch_coin: cc.AudioClip = null; @property({type:cc.AudioClip}) victory: cc.AudioClip = null; @property(cc.Node) Canvas: cc.Node = null; @property(cc.Node) MainCamera: cc.Node = null; @property(cc.Prefab) PauseBoard: cc.Prefab = null; // @property(Player) // mario: Player; private count: number = 0; private Win: boolean = false; // remain_lives: number = 0; levelText: cc.Label; timeText: cc.Label; gameLevel = 1; remainTime = 300; // 300 second gameover: boolean = false; initProperties(){ this.levelText = cc.find("Canvas/Main Camera/level1/TopContent/Level_one/level").getComponent(cc.Label); this.timeText = cc.find("Canvas/Main Camera/level1/TopContent/Time/time").getComponent(cc.Label); } updateUI(dt){ this.levelText.string = this.gameLevel.toString(); if(!this.Win){ this.remainTime -= dt; if(this.remainTime < 0){ this.remainTime = 0; } } // this.timeText.string = this.remainTime.toString(); this.timeText.string = this.remainTime.toFixed(0).toString().replace(".", ":"); } // private count : number = 0; playBGM(){ // ===================== TODO ===================== // 1. Play music. The audio clip to play is this.bgm cc.audioEngine.playMusic(this.bgm, true) // ================================================ } stopBGM(){ // ===================== TODO ===================== // 1. Stop music. cc.audioEngine.pauseMusic(); // ================================================ } resumeBGM(){ cc.audioEngine.resumeMusic(); } playEffect(){ // ===================== TODO ===================== // 1. Play sound effect. The audio clip to play is cc.log("Click."); cc.audioEngine.playEffect(this.touch_coin, false); // ================================================ } Toggle_init(){ let checkEventHandler = new cc.Component.EventHandler(); checkEventHandler.target = this.node; // 这个 node 节点是你的事件处理代码组件所属的节点 checkEventHandler.component = "level_1"; checkEventHandler.handler = "Pause"; checkEventHandler.customEventData = "foobar"; cc.find("Canvas/Main Camera/level1/UI/Pausebtn").getComponent(cc.Button).clickEvents.push(checkEventHandler); // this.toggle.checkEvents.push(checkEventHandler); } resume_init(){ let conti = new cc.Component.EventHandler(); conti.target = this.node; // 这个 node 节点是你的事件处理代码组件所属的节点 conti.component = "level_1"; conti.handler = "Resume"; conti.customEventData = "foobar"; cc.find("Canvas/pause/Continue").getComponent(cc.Button).clickEvents.push(conti); let quit = new cc.Component.EventHandler(); quit.target = this.node; // 这个 node 节点是你的事件处理代码组件所属的节点 quit.component = "level_1"; quit.handler = "Quit"; quit.customEventData = "foobar"; cc.find("Canvas/pause/Quit").getComponent(cc.Button).clickEvents.push(quit); } Resume(event, customEventData){ // Destroy pause board and resume the game! this.count = 0; cc.log("Resume"); cc.find("Canvas/pause").destroy(); cc.director.resume(); this.resumeBGM(); } Quit(event, customEventData){ // Quit game this.count = 0; cc.find("Canvas/pause").destroy(); cc.director.loadScene("setting"); } // EnablePause(){ // cc.find("Canvas/Main Camera/level1/UI/Pausebtn").on('click', this.Pause , this); // } // OFFPause(){ // cc.find("Canvas/Main Camera/level1/UI/Pausebtn").getComponent(cc.Button).onDisable(); // } Pause(event, customEventData) { // 这里的 toggle 是事件发出的 Toggle 组件 // 这里的 customEventData 参数就等于之前设置的 "foobar" // Pause. // Turn off button listener. // this.OFFPause(); this.count++; if(this.count==1){ // Draw UI Life : var pause = cc.instantiate(this.PauseBoard); this.Canvas.addChild(pause); pause.setPosition(this.MainCamera.x, this.MainCamera.y); // Init resume btns. this.resume_init(); cc.log("Pause"); this.stopBGM(); cc.director.pause(); }else{ cc.log("Cannot press Pause since Now pausing."); } } win(){ cc.log("win"); this.stopBGM(); // stop timer. this.Win = true; cc.audioEngine.playMusic(this.victory, false); var user = firebase.auth().currentUser; // num_of_times ++; firebase.database().ref('Users/'+user.displayName+'/num_of_times').once('value',(snapshot)=>{ // get current time, then increment it by one. firebase.database().ref('Users/'+user.displayName).update({ num_of_times: snapshot.val()+ 1 }).then(()=>{ // Log score data. firebase.database().ref('Users/'+user.displayName+'/num_of_times').once('value',(snapshot)=>{ // get current times. firebase.database().ref('Users/'+user.displayName+'/'+snapshot.val()).set({ scores : Number(cc.find("Canvas/mario").getComponent("Player").score), time: Number(300- Number(this.remainTime.toFixed(0))) }) }) }) }) // Check if need to update 'Best_score' firebase.database().ref('Users/'+user.displayName+'/Best_score').once('value',(snapshot)=>{ // get current times. if(cc.find("Canvas/mario").getComponent("Player").score.toString() > snapshot.val()){ // Update Best_score. firebase.database().ref('Users/'+user.displayName+'/').update({ Best_score: cc.find("Canvas/mario").getComponent("Player").score.toString() }) } }) // Update user lives. firebase.database().ref('Users/'+user.displayName+'/').update({ Lives: cc.find("Canvas/mario").getComponent("Player").lives }) setTimeout(()=>{ cc.director.loadScene("1to2"); return; }, 5000); } // LIFE-CYCLE CALLBACKS: onLoad () { this.initProperties(); this.gameover = false; } start () { this.playBGM(); this.Toggle_init(); } update (dt) { if(!this.gameover){ this.updateUI(dt); if(this.remainTime == 0){ this.gameover = true; this.onGameover(); } } } onGameover(){ cc.director.loadScene("Gameover"); } } ``` ### 3. Enemy_Turtle.ts 與烏龜相關的一切程式。從`onload()` `start()` `update(dt)` 三個核心**method**下去講解。 **(1)onload()**: 程式載入後做兩件事情: **1.開啟物理引擎** **2.初始化`this.anim`為`cc.Animation`物件** ``` typescript= onLoad() { cc.director.getPhysicsManager().enabled = true; this.anim = this.getComponent(cc.Animation); } ``` **(2)start()**: onLoad()後做四件事情: **1.取得rebornPos** **2.初始化linearVelocity** **3.初始化`this.isDead`為false** **4.初始化Animation為`Turtle_anim`** ``` typescript= start() { this.rebornPos = this.node.position; this.node.getComponent(cc.RigidBody).linearVelocity = cc.v2(-100, 0); this.isDead = false; this.animateState = this.anim.play('Turtle_anim'); } ``` **(3)update(dt)**: 每dt時間更新一次: **1.若`isDead`為True** **2.重生烏龜位置** **3.reset `this.isDead` 為 false** ``` typescript= update(dt) { if(this.isDead) { this.resetPos(); this.isDead = false; } } ``` :::info 本程式這次的**Turtle**部分沒有使用到`this.reborn()`,因為烏龜被踩扁後會變成龜殼,尚未做到讓烏龜死掉的部分 ::: * 完整的`Enemy_Turtle.ts`如下: ``` typescript= const {ccclass, property} = cc._decorator; @ccclass export default class Enemy_Turtle extends cc.Component { private beenhit: boolean = false; private anim = null; //this will use to get animation component private animateState = null; //this will use to record animationState private rebornPos = null; private isDead = true; onLoad() { cc.director.getPhysicsManager().enabled = true; this.anim = this.getComponent(cc.Animation); } start() { this.rebornPos = this.node.position; this.node.getComponent(cc.RigidBody).linearVelocity = cc.v2(-100, 0); this.isDead = false; this.animateState = this.anim.play('Turtle_anim'); } update(dt) { if(this.isDead) { this.resetPos(); this.isDead = false; } } public resetPos() { this.node.position = this.rebornPos; this.node.scaleX = 1; this.node.getComponent(cc.RigidBody).linearVelocity = cc.v2(-100, 0); } onBeginContact(contact, self, other) { if(other.node.name == "left_bound") { this.node.scaleX = -3.5; // turn right. this.node.getComponent(cc.RigidBody).linearVelocity = cc.v2(100, 0); } else if(other.node.name == "right_bound") { this.node.scaleX = 3.5; this.node.getComponent(cc.RigidBody).linearVelocity = cc.v2(-100, 0); } else if(other.node.name == "bullet") { this.isDead = true; other.node.destroy(); } else if(other.tag == 3){ // block. if(this.beenhit){ if(this.node.scaleX == -3.5){ //Mow moving right. this.node.scaleX = 3.5; // Turn Left. this.node.getComponent(cc.RigidBody).linearVelocity = cc.v2(-200, 0); // Move left. } else{ this.node.scaleX = -3.5; this.node.getComponent(cc.RigidBody).linearVelocity = cc.v2(200, 0); } } else{ if(this.node.scaleX == -3.5){ //Mow moving right. this.node.scaleX = 3.5; // Turn Left. this.node.getComponent(cc.RigidBody).linearVelocity = cc.v2(-100, 0); // Move left. } else{ this.node.scaleX = -3.5; this.node.getComponent(cc.RigidBody).linearVelocity = cc.v2(100, 0); } } }else if(other.node.name =="mario" || other.node.name=="mario_level2"){ if(this.beenhit){ if(contact.getWorldManifold().normal.y ==1){ // stop speed. this.node.getComponent(cc.RigidBody).linearVelocity = cc.v2(0, 0); // Move left. this.anim.stop('Turtle_spin'); } else{ // Just change direction. this.animateState = this.anim.play('Turtle_spin'); if(other.node.x > this.node.x){ // mario is right of us. this.node.scaleX = 3.5; // Turn Left. this.node.getComponent(cc.RigidBody).linearVelocity = cc.v2(-200, 0); // Move left. } else{ this.node.scaleX = -3.5; this.node.getComponent(cc.RigidBody).linearVelocity = cc.v2(200, 0); // Move right. } } } else{ if(contact.getWorldManifold().normal.y==1){ // Mario is on top of us. this.anim.play('Turtle_shrink'); this.node.getComponent(cc.RigidBody).linearVelocity = cc.v2(0, 0); this.beenhit = true; } else{ // Mario dead. } } } } // end onBeginContact. } ``` ###### tags: `Software Studio` `Software Lab` `軟體設計與實驗` `軟實`