2013/5/8

[Unity] Futile + GoKit 的打地鼠練習


為了多熟悉Futile及其內建的GoKit,於是做了個打地鼠的練習。
觀看範例


寫在前頭

這篇僅挑重點寫,若對Unity或Futile完全沒碰過的,請先看完上一篇再來。
另外,這篇也是參考Futile作者的教學影片來的,有興趣的話也可過去看看。


Texture打包

使用Texture Packer打包所需的圖形,設定上的重點是:
輸出Data Format選Unity3D
Layout的Algorithm選Basic, Trim Mode選None
然後按Publish輸出,本範例輸出命名為:textures.png及textures.txt

另外字型圖也可包進同一張Texture圖裏。我是用bmGlyph這套軟體做字型圖,我選"Impact"這個字型來做,產生了ImpactFont.png及ImpactFont.txt兩個檔案,然後ImpactFont.png這個也可拉進Texture Packer裏一起輸出。如下圖


所以最後會得到texture.png, texture.txt及ImpactFont.txt三個檔,就一併放到Unity專案路徑裏的/Assets/Resources/Atlases/裏吧

專案設定

本範例長寬設為960x640,其他就沒有特別做什麼設定了


Pixel Style

因為我個人滿偏好Pixel Style的,所以特別想知道Futile在開發Pixel Style時的設定方式,尤其Unity這種3D環境經常預設就會幫你做一些反鋸齒之類的效果,但這反而會把銳利的Pixel給模糊掉,因此首先要知道如何設定texture才能得到我們想要的效果。本範例對Texture的設定如下




Textures Atlas的載入與使用

上一篇只單純載入一張png圖,這次是把一堆圖及字型,都拼成一塊再載入,不過用法是差不多的:
Futile.atlasManager.LoadAtlas("Atlases/textures");
Futile.atlasManager.LoadFont("ImpactFont","ImpactFont.png","Atlases/ImpactFont");

//使用圖形時:
_background = new FSprite("background.png");

//使用字型時:
_label = new FLabel("ImpactFont","Tap the BaoZi to Start!!");


FSprite與FContainer

FSprite不能再包別的FSprite,更不能包FContainer。它只有一層,不像Flash那樣可以層層包。
FSprite在生成時直接就可以塞一張圖進去,FContainer不行。
只有FContainer可以包東西,除了可包FSprite外,也可包FContainer。
FContainer有scale, scaleX, scaleY等屬性,但卻沒有width, height。


FButton與 FLabel

//Button用法
_myBtn = new FButton("原本的圖.png", "壓下去的圖.png ", "音效名");
//event是用+的方式偵聽
_myBtn.SignalRelease += onTapFunction;
private void onTapFunction(FButton obj){
  Main.instance.gotoTitle();
}

//Label
_myLabel = new FLabel("字型名稱","文字內文");
_myLabel.text = "改字的內容"
//Label本身沒有width, height,要用textRect
Debug.Log(_myLabel.textRect.width)
Debug.Log(_myLabel.textRect.height)



TouchManager


//要先加入MultiTouch的介面
public class PageGame : FContainer, FMultiTouchableInterface {

override public void HandleAddedToStage(){
        //啟用
 Futile.touchManager.AddMultiTouchTarget(this);
 base.HandleAddedToStage();
}
 
override public void HandleRemovedFromStage(){
        //停用
 Futile.touchManager.RemoveMultiTouchTarget(this);
 base.HandleRemovedFromStage();
}

//touches包含了目前所有動作中的點的資訊
public void HandleMultiTouch(FTouch[] touches){
 foreach(FTouch _touch in touches){
  if(_touch.phase == TouchPhase.Began){
   //do something 
  }
 }
}


}



一些Event的設置


//把某個function加入到Futile系統的Update事件中,效果類似於Flash的EnterFrame
Futile.instance.SignalUpdate += myFunction;
private void myFunction () {
    //do something
}
//取消就用減的
Futile.instance.SignalUpdate -= myFunction;


//加入跟移出場景時的事件,名稱及效果都與Flash的類似,FSprite, FContainer, FButton這些都有這個事件
override public void HandleAddedToStage(){
} 
override public void HandleRemovedFromStage(){
}




FRenderLayer發生GameObject.active的問題

目前這個版本(0.67 alpha)在Unity 4.1的環境下,輸出時會出現類似這樣的錯誤訊息:
Assets/Plugins/Futile/Core/FRenderLayer.cs(110,29): warning CS0618: UnityEngine.GameObject.active' is obsolete:GameObject.active is obsolete. Use GameObject.SetActive(), GameObject.activeSelf or GameObject.activeInHierarchy.'
作者有在github上說明問題點,並已在dev版本修正了。所以下一版的Futile release時這問題應該就不會有了。
目前的話,可自行修改,改的地方只有三行,很容易。
打開FRenderLayer.cs這個檔,可找到三行 _gameObject.active = XXX; 的程式,自行改成 _gameObject.SetActive(XXX); 即可。
(這問題應該在下一版的Futile release後就不會吧)

GoKit的使用

GoKit是一個類似AS3裏的Tween Library,用法也類似,Futile有把GoKit包進來,所以可直接用
//基本用法,寫法跟TweenLite很像
Go.to( someTransform, 4f, new GoTweenConfig().position( new Vector3( 10, 10, 10 ) ) );
//由於GoKit不是只設計給Futile用的,而是設計給Unity用的,所以上一行會用3d的座標
//在Futile裏用的話,可以這樣寫
Go.to( instanceOfMyClass, 3f, new TweenConfig().floatProp("scale",scaleTarget*0.25f) );
//同時要變多個屬性的話,可以一直接在後面:
Go.to( instanceOfMyClass, 3f, new TweenConfig().floatProp("scale",scaleTarget*0.25f).floatProp("y",100) );

//也可以用TweenChain一次設定一整串Tween
var tween1 = new Tween(this, 0.1f, new TweenConfig().floatProp("scaleY",scaleTarget*0.3f).floatProp("scaleX",scaleTarget*1.4f).floatProp("y",_yTarget)); 
  var tween2 = new Tween(this, 0.1f, new TweenConfig().floatProp("scaleY",scaleTarget*1.4f).floatProp("scaleX",scaleTarget*0.3f).floatProp("y",_yTarget + height)); 
  var tween3 = new Tween(this, 0.1f, new TweenConfig().floatProp("scale",scaleTarget*0.25f).floatProp("y",_yTarget)); 
  
var chain = new TweenChain();
chain.append(tween1).append(tween2).append(tween3);
chain.setOnCompleteHandler(C=>{
  //do something 
 }
);
chain.play();


其他更詳細的用法,請自己上官網去看

最後是原始碼大爆發
Main.cs:
using UnityEngine;
using System.Collections;

public class Main : MonoBehaviour {

 private FContainer _currentPage;
 
 public static Main instance;
 
 // Use this for initialization
 void Start () {
   instance = this;
   FutileParams futileParams = new FutileParams (true,true,false,false);
   futileParams.AddResolutionLevel(960,1,1,"");
   futileParams.origin = new Vector2(0.5f,0.5f);
   Futile.instance.Init(futileParams);
   
   Futile.atlasManager.LoadAtlas("Atlases/textures");
   Futile.atlasManager.LoadFont("ImpactFont","ImpactFont.png","Atlases/ImpactFont");
   
   //FSprite _bg = new FSprite("background.png");
   //Futile.stage.AddChild(_bg);
  
   gotoTitle();
 }
 
 
 public void gotoTitle(){
  if(_currentPage != null)_currentPage.RemoveFromContainer();
  _currentPage = new PageTitle();
  Futile.stage.AddChild(_currentPage);
 }
 
 public void gotoGame(){
  if(_currentPage != null)_currentPage.RemoveFromContainer();
  _currentPage = new PageGame();
  Futile.stage.AddChild(_currentPage);
 }
 
 // Update is called once per frame
 void Update () {
 
 }
}



PageTitle.cs:
using UnityEngine;
using System.Collections;

public class PageTitle : FContainer, FSingleTouchableInterface {
 
 private FSprite _background;
 private FSprite _mainLogo;
 private FLabel _label;
 
 
 public PageTitle(){
  _background = new FSprite("background.png");
  AddChild(_background);
  
  _mainLogo = new FSprite("mainLogo.png");
  AddChild(_mainLogo);
  
  
  _label = new FLabel("ImpactFont","Tap the BaoZi to Start!!");
  _label.color = Color.white;
  _label.y = -260;
  _label.scale = 2;
  AddChild(_label);
  
  doBreath();
  doShake ();
  
  FContainer _test = new FContainer ();
  AddChild (_test);
  //var bb = new FButton(
  //bb.SignalRelease += onRelease;
  
 }
 
 
 override public void HandleAddedToStage(){
  Futile.touchManager.AddSingleTouchTarget(this);
 
  base.HandleAddedToStage();
 }
 override public void HandleRemovedFromStage(){
  Futile.touchManager.RemoveSingleTouchTarget(this);
  base.HandleRemovedFromStage();
 }
 
 public bool HandleSingleTouchBegan(FTouch touch)
 {
  //Debug.Log("Touch");
  return true;
 }
 public void HandleSingleTouchMoved(FTouch touch)
 {
  //Debug.Log("Move");
 }
 public void HandleSingleTouchEnded(FTouch touch)
 {
  //Debug.Log("Ended");
  Vector2 pos = _mainLogo.GlobalToLocal(touch.position);
  if(_mainLogo.textureRect.Contains(pos)){
   //Debug.Log("Oh");
   Main.instance.gotoGame();
  }
 }
 public void HandleSingleTouchCanceled(FTouch touch)
 {
  //Debug.Log("Canceled");
 }
 
 private void doBreath(){
  //Debug.Log("doBreath");
  var tween1 = new Tween(_mainLogo, 2.5f, new TweenConfig().floatProp("scale",1.05f).setEaseType(EaseType.QuadInOut)); 
  var tween2 = new Tween(_mainLogo, 2.5f, new TweenConfig().floatProp("scale",1).setEaseType(EaseType.QuadInOut)); 
  var chain = new TweenChain();
  chain.append(tween1).append(tween2);
  chain.setOnCompleteHandler(C=>{
    doBreath();
   }
  );
  chain.play();
 }
 
 private void doShake () {
  //Debug.Log("doShake");
  
  var r = RXRandom.Float()*16 - 8;
  
  var tween1 = new Tween(_mainLogo, 0.05f, new TweenConfig().floatProp("rotation",r).setEaseType(EaseType.BackOut)); 
  var tween2 = new Tween(_mainLogo, 0.1f, new TweenConfig().floatProp("rotation",-r).setEaseType(EaseType.BackOut)); 
  var tween3 = new Tween(_mainLogo, 0.1f, new TweenConfig().floatProp("rotation",r).setEaseType(EaseType.BackOut)); 
  var tween4 = new Tween(_mainLogo, 0.1f, new TweenConfig().floatProp("rotation",-r).setEaseType(EaseType.BackOut)); 
  var tween5 = new Tween(_mainLogo, 0.05f, new TweenConfig().floatProp("rotation",0).setEaseType(EaseType.BackOut)); 
  
  
  
  var chain = new TweenChain();
  chain.appendDelay(RXRandom.Float()*5+3).append( tween1).append(tween2).append(tween3).append(tween4).append(tween5);
  chain.setOnCompleteHandler(C=>{
   doShake();
  }
   );
  chain.play();
  
 }
 
 // Update is called once per frame
 void Update () {
  
 }
}


PageGame.cs:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class PageGame : FContainer, FMultiTouchableInterface {
 
 private FSprite _background;
 private FButton _exitBtn;
 private int _frameCount;
 private int _createBaoziTime;
 private int _baoziMaxCount;
 private int _liveTimeLimit;
 private FContainer _baozisContainer;
 private FContainer _baozisCrushContainer;
 private List _baoziList;
 private FLabel _scoreLabel;
 private FLabel _lifeLabel;
 private FLabel _overLabel;
 private int _score;
 private int _life ;
 private bool _isPlaying;
  
 
 //private List
 
 // Use this for initialization
 public PageGame () {
  //Debug.Log("OOOKKK");
  _background = new FSprite("background.png");
  AddChild(_background);
  
  init();
 }
 
 private void setDiffcult(){
  if(_score < 10){
   _createBaoziTime = 45;
   _baoziMaxCount = 5;
   _liveTimeLimit = 300;
  }else if(_score < 30 && _score >=10){
   _createBaoziTime = 40;
    _baoziMaxCount = 10;
   _liveTimeLimit = 250;
  }else if(_score < 70 && _score >=30){
   _createBaoziTime = 30;
    _baoziMaxCount = 15;
   _liveTimeLimit = 250;
  }else if(_score < 100 && _score >=70){
   _createBaoziTime = 25;
    _baoziMaxCount = 20;
   _liveTimeLimit = 200;
  }else if(_score >=100){
   _createBaoziTime = 20;
   _baoziMaxCount = 25;
   _liveTimeLimit = 150;
  }
  
  
  
 }
 
 private void init(){
 
  _baoziList = new List();
  
  _baozisContainer = new FContainer();
  AddChild(_baozisContainer);
  
  _baozisCrushContainer = new FContainer();
  AddChild(_baozisCrushContainer);
  
  
  _scoreLabel = new FLabel("ImpactFont","SCORE: 000");
  _scoreLabel.y = Futile.screen.halfHeight-32;
  _scoreLabel.scale = 2;
  _scoreLabel.x = -Futile.screen.halfWidth+_scoreLabel.textRect.width;
  
  AddChild(_scoreLabel);
  
  
  _lifeLabel = new FLabel("ImpactFont","LIFE: 010");
  _lifeLabel.y = Futile.screen.halfHeight-32;
  _lifeLabel.scale = 2;
  _lifeLabel.x = Futile.screen.halfWidth-_lifeLabel.textRect.width;
  
  AddChild(_lifeLabel);
  
  
  _overLabel = new FLabel("ImpactFont","GAME OVER");
  _overLabel.scale = 5;
  AddChild(_overLabel);
  
  
  _exitBtn = new FButton("exit.png");
  AddChild(_exitBtn);
  _exitBtn.SignalRelease += onClickExit;
  
  _exitBtn.x = Futile.screen.halfWidth-_exitBtn.sprite.width/2 -8;
  _exitBtn.y =  -Futile.screen.halfHeight + _exitBtn.sprite.height/2 + 8;
  
  
 }
 
 private void onClickExit(FButton obj){
  Main.instance.gotoTitle();
 }
 
 
 
 override public void HandleAddedToStage(){
  _frameCount = 0;
  _score = 0;
  setDiffcult();
  _life = 10;
  _isPlaying = true;
  _overLabel.isVisible = false;
  Futile.instance.SignalUpdate += signalUpdate;
  Futile.touchManager.AddMultiTouchTarget(this);
  base.HandleAddedToStage();
 }
 
 override public void HandleRemovedFromStage(){
  Futile.instance.SignalUpdate -= signalUpdate;
  Futile.touchManager.RemoveMultiTouchTarget(this);
  base.HandleRemovedFromStage();
 }
 
 private void addScore(){
  
  _score ++;
  setDiffcult();
  string _str = _score.ToString();
  while(_str.Length < 3){
   _str = "0"+_str;
  }
  _scoreLabel.text = "SCORE: "+_str;
 }
 
 
 
 
 public void HandleMultiTouch(FTouch[] touches){
  if(!_isPlaying)return;
  foreach(FTouch _touch in touches){
   if(_touch.phase == TouchPhase.Began){
    for(int i = _baoziList.Count -1; i>=0; i--){
     Baozi _baozi = _baoziList[i];
     Vector2 _touchPos = _baozi.GlobalToLocal(_touch.position);
     if(_baozi.textureRect.Contains(_touchPos)){
      crushBaozi(_baozi);
      addScore();
      break;
     }
    }
    
   }
  }
 }
 
 private void decLife(){
  _life --;
  string _str = _life.ToString();
  while(_str.Length < 3){
   _str = "0"+_str;
  }
  _lifeLabel.text = "LIFE: "+_str;
  if(_life<=0){
   gameOver();
  
  }
 }
 
 private void gameOver(){
  _overLabel.isVisible = true;
  _isPlaying = false;
 }
 
 // Update is called once per frame
 private void signalUpdate () {
  
  if(!_isPlaying)return;
  
  if(_frameCount % _createBaoziTime ==0 && _baoziList.Count<_baoziMaxCount){
   createBaozi();
  }
  _frameCount++;
  
  for(int i = _baoziList.Count -1; i>=0; i--){
   Baozi _baozi = _baoziList[i];
   
   _baozi.liveTime ++;
   if(_baozi.liveTime >= _liveTimeLimit){
    _baoziList.Remove(_baozi);
    _baozi.doHiding();
    decLife ();
    //_baozi.RemoveFromContainer();
   }
   //Vector2 _touchPos = _baozi.GlobalToLocal(_touch.position);
   //if(_baozi.textureRect.Contains(_touchPos)){
    //crushBaozi(_baozi);  
    //break;
   //}
  }
  
 }
 
 private void crushBaozi(Baozi _baozi){
  BaoziCrush _baoziCrush = new BaoziCrush();
  //_baoziCrush.scale = _baozi.scale;
  _baoziCrush.x = _baozi.x;
  _baoziCrush.y = _baozi.y-_baozi.height*0.3f;
  _baoziCrush.setCrush(_baozi.scaleTarget);
  
  _baozisCrushContainer.AddChild(_baoziCrush);
  _baoziList.Remove(_baozi);
  _baozi.RemoveFromContainer();
  
 }
 
 private void createBaozi(){
  Baozi _baozi = new Baozi();
  //_baozi.x = RXRandom.Range(-Futile.screen.halfWidth,Futile.screen.halfWidth);
  //_baozi.y = RXRandom.Range(-Futile.screen.halfHeight,Futile.screen.halfHeight);
  _baozisContainer.AddChild(_baozi);
  _baozi.doIntro();
  _baoziList.Add(_baozi);
 }
}


Baozi.cs:
using UnityEngine;
using System.Collections;

public class Baozi : FSprite {
 public float scaleTarget;
 public int liveTime;
 private float _yTarget;
 // Use this for initialization
 public Baozi():base ("baozi.png"){
  
  x = RXRandom.Range(-Futile.screen.halfWidth + width/2,Futile.screen.halfWidth - width/2);
  _yTarget = y = RXRandom.Range(-Futile.screen.halfHeight + 100,Futile.screen.halfHeight - height/2-32);
  scaleTarget = scale = (1-(_yTarget+Futile.screen.halfHeight)/Futile.screen.height) + 0.3f;
  //scaleTarget = scale = RXRandom.Float()*0.5f + 0.5f;
 }
 
 public void doIntro(){
  
  //_scaleTarget *=1.05f;
  this.scale = scaleTarget*0.25f;
  var tween1 = new Tween(this, 0.1f, new TweenConfig().floatProp("scaleY",scaleTarget*1.2f).floatProp("y",_yTarget + height*2)); 
  var tween2 = new Tween(this, 0.1f, new TweenConfig().floatProp("scaleY",scaleTarget*0.3f).floatProp("scaleX",scaleTarget*1.4f).floatProp("y",_yTarget)); 
  var tween3 = new Tween(this, 0.1f, new TweenConfig().floatProp("scale",scaleTarget).setEaseType(EaseType.BackOut)); 
  
  var chain = new TweenChain();
  chain.append(tween1).append(tween2).append(tween3);
  chain.setOnCompleteHandler(C=>{
    liveTime = 0;
   }
  );
  chain.play();
 }
 
 public void doHiding(){
  
  var tween1 = new Tween(this, 0.1f, new TweenConfig().floatProp("scaleY",scaleTarget*0.3f).floatProp("scaleX",scaleTarget*1.4f).floatProp("y",_yTarget)); 
  var tween2 = new Tween(this, 0.1f, new TweenConfig().floatProp("scaleY",scaleTarget*1.4f).floatProp("scaleX",scaleTarget*0.3f).floatProp("y",_yTarget + height)); 
  var tween3 = new Tween(this, 0.1f, new TweenConfig().floatProp("scale",scaleTarget*0.25f).floatProp("y",_yTarget)); 
  
  var chain = new TweenChain();
  chain.append(tween1).append(tween2).append(tween3);
  chain.setOnCompleteHandler(C=>{
    this.RemoveFromContainer();
   }
  );
  chain.play();
  
  
  
  
 }
 
 // Update is called once per frame
 void Update () {
 
 }
}


BaoziCrush.cs:
using UnityEngine;
using System.Collections;

public class BaoziCrush : FSprite {

 // Use this for initialization
 public BaoziCrush():base ("baoziCrush.png"){
  //scale = RXRandom.Float()*0.5f + 0.5f;
  
  //x = RXRandom.Range(-Futile.screen.halfWidth + width/2,Futile.screen.halfWidth - width/2);
  //y = RXRandom.Range(-Futile.screen.halfHeight + 100,Futile.screen.halfHeight - height/2);
 }
 
 public void setCrush(float _scaleTarget){
  _scaleTarget *=1.05f;
  this.scale = _scaleTarget/2;
  var tween1 = new Tween(this, 0.1f, new TweenConfig().floatProp("scale",_scaleTarget).setEaseType(EaseType.BackOut)); 
  var tween2 = new Tween(this, 1.0f, new TweenConfig().floatProp("alpha",0f).setEaseType(EaseType.QuadInOut)); 
  var chain = new TweenChain();
  chain.append(tween1).append(tween2);
  chain.setOnCompleteHandler(C=>{
    this.RemoveFromContainer();
    //Debug.Log("Killed");
   }
  );
  chain.play();
  
 }
 
 //private void destory(){
 // this.RemoveFromContainer();
  
 //}
 // Update is called once per frame
 void Update () {
 
 }
}



寫在後面

接下來的目標是音效音樂的使用,以及近期會被導入的Spine動畫,還有一些gitHub上別人開發給Futile的小外掛。謝謝收看。