2014/9/4

[筆記]從Flash到LWF再到Unity


簡單記錄一下如何透過LWF將Flash的動畫輸出到Unity裏運用。


§ 安 裝 §


.在Unity開啟一個新專案。

.到LWF官網 http://gree.github.io/lwf/ 下載最新的版本。

.解壓縮後,在解壓目錄/csharp/unity/dist/ 下可以找到一個lwf.unitpackage,執行它並安裝到剛剛開的unity專案裏。

[註]最新版的LWF需要Unity4.5以上環境

以上就做完Unity這邊初步的環境建置。接著是準備Flash的轉檔環境。


LWF官網本身有提供JSFL方式的轉檔script,可在剛剛的壓縮裏找到。不過網路上有另一個更方便的工具LWFS。


.到 https://github.com/gree/lwfs/releases下載最新版的LWFS,有分OSX及Wins版,要注意看一下。

.解開後會得到一個LWFS的目錄,裏面有start.app及stop.app,是兩支python寫的程式。執行start.app,正常的話會自動啟動瀏覽器,並出現類似這樣的畫面:




另外還會自動在桌面上產生兩個資料夾:LWFS_work及LWFS_work_output


[註]Mac OSX 10.9以上的話,要先針對LWFS資料夾的執行權限做設定,依照裏面README.txt寫的步驟去做就可以了。
[註2]LWFS放置的路徑中,最好不要有中文、空格之類字元出現,否則start.app可能會無法執行。



§ 轉 檔 §


.把swf檔案放在桌面上的LWFS_work資料夾內,幾秒內就會自動轉檔,產生的檔案會在LWFS_owrk_output裏。

.swf在製作上有一些注意事項,請參照官網提供的Flash Production Guideline(pdf)

.另外如果要用程式控制場景上的MovieClip,則必需設定好instance name。

.若要動態從Library裏叫出做好的元件,則需要設定好Linkage id。




§ 匯 入 §


.swf成功轉檔的話,會在LWFS_work_output/unity/找到與swf同名的資料夾,例如test.swf會輸出一個test.swf的資料夾,裏面應該會有素材圖檔及一個test.bytes的文字檔。

.將整個test.swf資料夾複制到Unity專案底下的Assets/Resource s/裏。

  void Start () {
  gameObject.AddComponent<LWFObject>();
  LWFObject _lwfObj = gameObject.GetComponent<LWFObject>();
  _lwfObj.Load("test.swf/test","test.swf/");//.bytes文字檔的路徑及存放素材圖檔的路徑
  LWF.Movie _movie = _lwfObj.lwf.rootMovie.AttachMovie("linkage_id","instance_id");
 }


.如果是放在stage上的動畫,在_lwfObj.Load這行執行後就會出現了。

.lwf.rootMovie.AttachMovie這段則是從Library中叫出動畫。

.更多程式控制的方式要多參閱官網的文件,另外LWF似乎以日本使用者居多,所以也可以找到一些日文blog有使用心得。



(update 2014/9/7)
後來在官網看到目前scale上似乎是有問題的,作者建議素材的texture設定如下:
Texture Type: Advanced
Non Power of 2: None
Generate Mip Map: Unchecked
Wrap Mode: Clamp
Filter Mode: Point (Bilinear is okay if the scale is one)




§ 參 考 §


LWF official page

LWF Demo page

Unity API Reference

LWF wiki For Flash (JP only)

LWFS

Notes on Lightweight SWF (LWF) framework for Unity(裏面的LWFAnimator已無法在最新版的LWF使用,不過一些語法可以參考)


2013/6/12

TexturePacker的十個好棒啊


使用TexturePacker也有一段時間了,今天就來介紹一下他的一些優點
由於網路上已有不少介紹文,且這軟體本身也不難操作,所以我就簡單列一下我喜歡這工具的十個優點。

**TexturePacker官網


1.支援各平台,Win, Mac, Linux都可以用,好棒啊~


2.使用超方便,拉進圖檔,設定格式,最後按一下Publish!不用20秒就搞定,好棒啊~


3.支援30多種輸出格式,不爽還可以自己定義新的,好棒啊~

支援目前絕大多數的2D遊戲引擎、Framework,共30多種,如果這些還沒有你要的,也可以自行定義你要的格式。
**自定義格式參考文件

4.可自定義多個解析度需求,一鍵輸出所有格式,AutoSD功能好棒啊~

尤其是同時要輸出Retina及非Retina的素材時,以往是需要返覆切換設定,各別按Publish到不同的路徑,現在只要在AutoSD裏設定好,一鍵就可輸出了。
**詳細可參考fallhunter的這篇TexturePacker 3.0 使用教程

5.方便把SWF轉成SpriteSheet,好棒啊~

用過幾套SWF轉SpriteSheet的工具,但大部份就只做到把SWF的圖擷出來再併成一張而已,一些細節設定就比較少。而大多專門在做合併SpriteSheet的工具軟體則不一定都會支援SWF import的功能。所以兩者兼備的TexturePacker確實是值得推薦。 **關於SWF輸出功能,可參考Gray Liao這篇「再次介紹TexturePacker」

6.SmartFolder,資料夾有任何變動就會自動更新,好棒啊~
做遊戲的過程經常是在修修改改的,圖的素材也是會增增減減的,如果每次變動就要重拉一次圖檔那就很累了。SmartFolder是只要你指定這個Folder為素材來源後,Folder只要有變動,TexturePacker就會自動更新(但還是要記得按Publish啦),確實很方便

7.支援十多種圖檔格式,包括Photoshop沒有的RGBA4444, PVRTC等,好棒啊~

**關於PVRTC,可參考這篇「助你開發iOS game的基礎知識」
**關於RGBA4444,可參考這篇「使用 RGBA4444 與 Dithering 減少記憶體用量」

8.功能完整的免費版、試用版還有佛心推廣版,好棒啊~
TexturePacker下載後,可選擇使用免費版(輸出圖會有浮水印),或是完整功能七天試用版,另外針對開發者或Blog作者也提供了佛心版,只要符合TexturePacker提出的條件,就可以跟他申請免費的License,請參考這頁的申請書

9.Xcode指令整合,省下很多重複動作的時間,好棒啊~
如果是使用Xcode開發的話,TexturePacker有提供與Xcode整合的指令,使用方式請參考這篇Tutorial: XCode4 integration of TexturePacker for Cocos2d and Sparrow framework

10.素材加密保護功能(Cocos2d Only),好棒啊~
怕畫好的圖素被偷走嗎?新版的TexturePacker提供了素材加密的功能,但目前只有Cocos2D-iPhone版有支援,未來也許會再支援到其他平台,詳細請參考這篇Protect your game assets (currently cocos2d only)


看完這麼多個好棒啊,不會想趕快來用看看嗎?快上TexturePacker官網試用看看吧!!

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的小外掛。謝謝收看。

2013/3/31

[Unity] Futile 2D Framework 試用心得



Futile是一套給Unity 用的2D Framework, 架構類似於AS3及Cocos2D,有用過這兩種的人會很容易上手。


官網 & GitHub位置: https://github.com/MattRix/Futile

Demo及入門教學影片: http://struct.ca/futile/

最近上架的Nimble Quest即是用Futile所開發

這是我目前試用的幾個Unity 2D方案中,最適合Flash/AS3人員轉移的了。原因是Futile不需要在Unity IDE上做太多操作,大部份都是用code來完成的,有點像是在Flash Builder上做事。唯一差別是寫C#而非AS3。

而沒寫過C#的人也不用太擔心,大致上寫起來不會太難,與AS3也有許多相似之處,尤其Futile延用了許多AS3人員熟悉的概念(Sprite, Container, AddChild...etc.) 以致上手時備感親切。


實際操作

1.於Unity裏建立一個新project(File -> New Project...)

2.至GitHub網站下載Futile的檔案

3.解開zip後,開啟裏面的Futile.unitypackage或是在Unity裏從Assets->Import Package->Custom Package…去安裝Futile


4.至以上步驟為止,我們已經把Futile安裝至我們的Project中了。

5.在Unity裏,把預設的Main Camera刪除掉


6.建立一個空白的GameObject


7.為方便辨識,在Inspector面板裏把新建的Game Object重新命名為"FutileObject"


8.在專案資料夾中,Assets/Plugins/Futile/裏放著Futile的所有程式檔。這時需先把主程式Futile.cs與剛建立的FutileObject綁定在一起。可以從Project面板把Futile.cs直接拉到Hierarchy面板中的FutileObject中,如下圖


9.可以在Inspector面板中確認Futile是否有綁進FutileObject裏


10.為了方便之後的開發,我們可以在Assets裏新建一個"Scripts"的資料夾,放置我們自己寫的.cs檔


11.在Scripts裏新建一個C# script檔,命名為"MyGame"



12 .接著我們試著在Futile裏呈現一張圖。Futile預設的素材載入路徑為/Assets/Resources/,所以先隨便找一張圖放在此處。(我放了一張名為baozi.png的圖)


13.接下來在Project面板中點兩下MyGame.cs,此時Unity會自動開啟MonoDevelop,這是Unity自帶的程式撰寫工具。以下是MyGame.cs需撰寫的內容:

using UnityEngine;
using System.Collections;

public class MyGame : MonoBehaviour {

 // Use this for initialization
 void Start () {
  FutileParams futileParams = new FutileParams(true,true,true,true);
  
  futileParams.AddResolutionLevel(480,1,1,"");
  
  Futile.instance.Init(futileParams);
  
  Futile.atlasManager.LoadImage("baozi");
  
  FSprite mySprite = new FSprite("baozi");
  Futile.stage.AddChild(mySprite);
 }
 
 // Update is called once per frame
 void Update () {
 
 }
}





14.最後記得把MyGame.cs也拉進FutileObject裏


15.點擊preview看看圖片是否有正常出現在畫面中


16 .在撰寫FutileParams及AddResolutionLevel等程式時,MonoDevelop就會自動出現各參數的解說了,在此就不解釋了。

17.Update這個event會有點類似我們在Flash裏用的EnterFrame,是一個持續執行的function,我們改一下剛剛的程式:

using UnityEngine;
using System.Collections;

public class MyGame : MonoBehaviour {
 
 private FSprite _mySprite;
 
 // Use this for initialization
 void Start () {
  FutileParams futileParams = new FutileParams(true,true,true,true);
  
  futileParams.AddResolutionLevel(480,1,1,"");
  
  Futile.instance.Init(futileParams);
  
  Futile.atlasManager.LoadImage("baozi");
  
  _mySprite = new FSprite("baozi");
  Futile.stage.AddChild(_mySprite);
 }
 
 // Update is called once per frame
 void Update () {
  _mySprite.rotation ++;
 }
}



18.正確的話,應該會看到載入的圖片不斷的轉動,程式中的rotation就如同AS3的使用方式,也可以改成x, y, alpha, scale等屬性試試。


注意: Futile的座標系統與Flash大不相同,Futile的(0,0)在畫面的正中間,y值向上為正,向下為負。


以上便算完成一個最基本的入門範例。除了一開始的刪除Main Camera,建新Game Object,以及把程式與Game Object設綁定之外,幾乎所有開發都是在MonoDevelop裏寫code。這點對於尚不熟悉Unity介面的人來說,是一個不錯的優點。

下一篇應該會來寫一個簡單的打地鼠遊戲。(其實也只是照著Futile作者的教學影片做變化而已,有興趣的人不妨自己去官網看影片會比較快)

2013/3/3

Spriter好好玩系列 (四) ---- 使用SpriterMC


說在前頭:
如果你看到上一篇的SpriterAS已經覺得很夠用的話,那這篇SpriterMC其實可以直接跳過。原因是我個人認為SpriterAS目前的實用性大過於SpriterMC,而寫這篇只是因為都花時間研究了,就留下點筆記而已。另外SpriterMC唯一較強的是目前有支援Spriter的Bones及IK,所以除非有此特別需求或是好奇想看看的話,個人建議可直接跳過。
(以上心得基於SpriterMC v1.3版的狀況)

.官網:http://www.sammyjoeosborne.com/SpriterMC/

.github載點:https://github.com/sammyjoeosborne/spritermc

.基本用法:http://wiki.starling-framework.org/extensions/spritermc#usage

先看一下範例:

(click to open)
Download Source

載入SCML:
在官網中是這樣寫的:
var monster1:SpriterMC = SpriterMCFactory.createSpriterMC("monster", "xml/monster.scml");
monster1.play(); //Note: SpriterMC's will not actually start playing or show up on stage until SpriterMC.SPRITER_MC_READY is broadcast
 
//Add each SpriterMC to a Juggler, just like a regular Starling MovieClip
myJuggler.add(monster1);
但他們另外提供了事先做好TextureAtlas的做法,並且推薦用這種:
   var monster1:SpriterMC = SpriterMCFactory.createSpriterMC("monster", "xml/monster.scml", _textureAtlas);
monster1.playbackSpeed = 1.5; //Demonstrating playbackSpeed, which is like Scale, 1 == 100%. You can also set negative values to play backward
monster1.play(); //Note: SpriterMC's will not actually start playing or show up on stage until SpriterMC.SPRITER_MC_READY is broadcast
 
//Add each SpriterMC to a Juggler, just like a regular Starling MovieClip
myJuggler.add(monster1);
但我個人測試結果是,你必需使用TextureAtlas的寫法,否則使用第一種的話,執行時會出現error。

如何使用TextureAtlas:
照SpriterMC官網的說明,在他解析並產生這些SCML動畫時,會動態將所有的元件圖檔拼成一張大張的TextrueAtlas,這樣更能符合Starling的圖形加速處理,所以他建議使用者不如一開始自己就先手動做好拼圖這個動作。這件事我們可以利用TexturePacker來做(格式選AS3/Starling)。做完會像這樣:

一張大拼圖及一個xml描述檔。而AS3寫法可參考以下:
protected function init():void{
   loadTexture('assets/heroTA.png') 
  }
  
  private function loadTexture(_url:String):void
  {
   var _loader:Loader = new Loader();
   _loader.contentLoaderInfo.addEventListener(starling.events.Event.COMPLETE, textureLoadedHandler);
   _loader.load(new URLRequest(_url));
  }
  
  private function textureLoadedHandler(e:*):void 
  {
   
   _heroTexture = Texture.fromBitmap(Bitmap(e.target.loader.content));
   loadTextureAtlasXML("assets/heroTA.xml");
  }
  
  private function loadTextureAtlasXML(_url:String):void 
  {
   var _urlLoader:URLLoader = new URLLoader(new URLRequest(_url));
   _urlLoader.addEventListener(starling.events.Event.COMPLETE, atlasXMLLoadedHandler);
  }
  
  private function atlasXMLLoadedHandler(e:*):void 
  {
   var _xml:XML = XML(e.target.data);
   _textureAtlas = new TextureAtlas(_heroTexture, _xml);
   createCharacters();
  }
  
  private function createCharacters():void
  { 
    _hero = SpriterMCFactory.createSpriterMC("heroIK", "assets/heroIK/heroIK.scml",_textureAtlas,spriterReadyHandler,true);
  }
  private function spriterReadyHandler(e:starling.events.Event):void 
  {
   _hero.x= 100
   _hero.y=380
   
   
   _hero.setAnimationByName('idle')
   _hero.play();
   
   addChild(_hero);
   Starling.juggler.add(_hero)
   
  }

頭尾相連的動畫
成功讀入scml後,首先會發現動畫有點問題,就是原本來Spriter裏,最後一個keyframe都會自動做tween連回第一個keyframe,除非你自己取消這個功能。但是從SpriterMC讀進來的動畫卻完全都沒有這個效果。

正常來說,我們在Spriter裏可以透過上圖a點來切換是否要開啟"頭尾相連"的功能,但在SpriterMC裏目前完全無效,所以變通的方法就是自行將0ms的keyframe複製到動畫最尾處,如上圖b點。

動畫播完的callBack
這個功能對遊戲開發十分重要,例如我們做完一個攻擊、做完一個跳躍動畫,都會需要馬上切換至一般狀態的動畫,所以動畫播完呼叫一個callback會是十分常做的事情。在SpriterAS裏有提供了addCallBack這樣的method,但SpriterMC目前尚未提供這樣的api,我只能先用變通的方式來處理。
   private function onClickJump(e:MouseEvent):void{
   if(_hero.currentAnimation.name == 'jump')return;
   
   _hero.setAnimationByName('jump')
   _hero.loop=false;
   _hero.play()
   _hero.addEventListener(starling.events.Event.ENTER_FRAME,onEnterCheck);
  
  }
  
  protected function onEnterCheck(e:starling.events.Event):void{
   if(_hero.isComplete){
    _hero.removeEventListener(starling.events.Event.ENTER_FRAME,onEnterCheck);
    _hero.setAnimationByName('idle')
    _hero.loop=true;
   }
  }
這邊我使用ENTER_FRAME不斷去check他的isComplete是否為true,如此才能知道他是不是播完了。而在使用isComplete時,有兩個注意事項:
1.需把loop設為false,如此isComplete才有機會變成true,否則會永遠得不到true。
2.我個人懷疑這是bug。當我設_hero.setAnimationByName('jump')並成功播完一次後,下次再執行_hero.play()時,isComplete會在一開始就是true了,也就是它並沒有因為重新play,而重設為false。為了解決這問題,我直接修改了SpriterMC裏Animation.as這支檔案的play函式:
internal function play():void
  {
   //if we aren't looping and play was called while the animation was on its last frame, restart it
   if (!_loop && _currentKeyIndex == _lastFrameIndex)
   {
    _currentTime = mainKeys[_firstFrameIndex].time; //this is gonna make currentTime 0 or the length of the animation, depending on which way the animation is playing
    _currentKeyIndex = _firstFrameIndex;
    
   }
   _animationEnded = false;//maso add
   _isPlaying = true;
  }
修改過後就能正常執行了。

其他心得
SpriterMC目前能做的應用很少,所以試到這裏就沒什麼好繼續了,所以其他請自行參閱官網。倒是有一點較特別的是,Spriter的timeline是以ms為單位,但SpriterMC裏卻提到了一個叫currentFrame屬性,這裏試了一下,所謂的frame其實是Spriter中的Keyframe,第一個Keyframe得到是0,第二個是1,以此類推。不過在沒有call back的應用下,這個currentFrame其實也沒太大的用處。



Index:
Spriter好好玩系列 (一) ---- 什麼是Spriter ?
Spriter好好玩系列 (二) ---- Spriter 基本操作
Spriter好好玩系列 (三) ---- 使用SpriterAS
Spriter好好玩系列 (四) ---- 使用SpriterMC

2013/3/2

Spriter好好玩系列 (三) ---- 使用SpriterAS


這篇要來把動畫跟AS3串接起來。用SpriterAS+Starling來做。

SpriterAS是一款基於Starling架構,專門播放Spriter動畫的AS3 Library。由加拿大的TreeFortress小組所開發。(TreeFortress專注於Mobile Game開發,成員包括了知名的gskinner大神)

.先看一下範例:

(click to open)
Download Source

上述範例我在Spriter裏做了三則animation,分別是idle(平常狀況)、fight_0(輕拳)、fight_1(重拳)。其他如眨眼、拳頭上的特效、頭掉下來,都是由程式控制的,而SpriterAS也支援Playback Speed的控制,這也是突顯了元件動畫優於連續圖檔的一大強項。

TreeFortress有寫了一篇Introducting SpriterAS - Play Spriter Animations with Starling。算是SpriterAS的入門介紹文,本篇範例也是看完他們的介紹文後的產物。建議有空可以先去看看,以下我只挑幾個重點介紹。

.github載點 https://github.com/treefortress/SpriterAS

.載入scml並叫出動畫
//**注意:SpriterAS是建立在Starling之上的,必需在Starling內
//**使用下列程式才能有作用。

protected var spLoader:SpriterLoader;

protected function init():void{
   
 spLoader = new SpriterLoader();
 spLoader.completed.add(onSpriterLoadComplete);
 //SpriterLoader可一次載入多個scml檔,所以參數是填一個array,可代入多個scml位置
 spLoader.load(['assets/hero/hero.scml'],1)
  
   
}
protected function onSpriterLoadComplete(spLoader:SpriterLoader):void
  {
   //從loader中取得動畫,以scml名稱為索引
   hero = spLoader.getSpriterClip('hero')
   //idle是hero.scml裏其中一段animation的名稱
   hero.play("idle")
   hero.x = 100
   hero.y =300
   addChild(hero)
   Starling.juggler.add(hero) 
}
這裏需要注意一點是,spLoader必需是class內的固定成員,不可以是function中的區域變數,例如我試過以下這樣寫的話,onSpriterLoadComplete將無法被觸發:
protected function init():void{
   
 var spLoader:SpriterLoader = new SpriterLoader();//不能用區域宣告
 spLoader.completed.add(onSpriterLoadComplete);
 spLoader.load(['assets/hero/hero.scml'],1)
  
   
}


.按下button切換animation至"fight_0"(輕拳),並在播放完畢時,切回"idle"
private function onClickFight0(e:MouseEvent):void
  {
   hero.play("fight_0")
   hero.addCallback(onOverFight,400)//400ms後呼叫onOverFight
}
protected function onOverFight():void{
 if(hero.animation.name != 'idle')hero.play("idle")
} 
直接使用 play("animation名稱"),即可切換動畫。
而 addCallback則是一個相當實用的method,可設定動畫播到多少ms時回call某個function,而此ms數會依照當初在Spriter裏設定的timeline來執行,因此不會受playback speed而受影響。
完整使用方式是:
addCallback(回call的function, 回call時間, 是否只執行一次)

.眨眼功能(動態切換元件)
private function onClickBlink(e:MouseEvent):void
  {
   hero.swapPiece("head_0", "head_1");
   TweenLite.delayedCall(.1,hero.unswapPiece,['head_0'])
}
swapPiece("A", "B")可將動畫中的A圖換成B圖。
unswapPiece("A")則是取消加諸在"A"身上的任何swapPiece效果。

.附著在拳頭上的粒子效果:
private function getHandPos():Point{
  var handImg:Image //Starling中的Image
  handImg = hero.getImage("leftHand_0");
  return new Point(handImg.x,handImg.y)
}
粒子效果非本篇重點,所以特效的部份就不多寫,上述程式只針對取得拳頭位置做示範,不過這邊要注意,取得的handImg.x及y後,記得還要加上hero.x及y,才是拳頭在畫面上看到的位置。

.頭掉下來(動態將元件拆離scml設定,或再組合回去)
private function onClickDropHead(e:MouseEvent):void
  {
   var headImg:Image = hero.getImage("head_0")
   if(headImg.y>-100){//弱弱的用y的位置來判斷目前頭的狀況
    hero.includePiece(headImg);//組回
    return;
   }
   
   hero.excludePiece(headImg,true)//脫離
   
   TweenLite.to(headImg,.6,{y:25,ease:Bounce.easeOut})
  }

.playback speed 控制
private function onSlideFPS(e:Event):void
  {
   _stage.frameRate = _fpsSlider.value
  } 
  private function onSlideSpeed(e:Event):void
  {
   hero.playbackSpeed = _speedSlider.value
  } 
這邊就沒什麼好解說的了,playbackSpeed代1的話表示正常進度,2為兩倍,0.5為半速,以此類推。

.動態才用到的元件
這邊算是個人使用心得。當我在製作範例中的眨眼功能,遇到了點小問題。原本我在Spriter裏編輯時只有三段animations,分別是idle, fight_0, fight_1,而這三段動畫完全沒用到眨眼的那張圖(head_1.png),使得當我在用hero.swapPiece("head_0", "head_1")時,會完全沒有做用。原因是當我查看scml檔的內容,發現scml在最前頭會先將所有圖檔列一份清單,之後的keyframe資訊就只會記下清單中的id,而這時又發現scml內只會記錄所有animation有用到過的圖檔,而不是所有放在project folder中的圖檔,所以它並沒有將我的head_1.png存進去,導致讓swapPiece("head_0", "head_1")無效。
變通的辨法是,再開一個新的animation,把所有程式會用到的圖都放進去,再輸出成scml即可。

例如上圖我就是把程式才會用到的圖,都放在"assets"這段animation裏。

.心得與注意事項:
SpriterAS使用下來,我個人覺得功能算是很完整了,已能符合大多數需求,唯一缺點是Spriter中的Bones功能尚未支援(如果有帶bone的scml,在spriterAS裏會爛掉),SpriterAS在官方blog是說未來會支援。不過我個人心得是,這部份還好,在我們真的在調比較細緻的動畫時,bones及IK其實沒什麼太大的幫助。另外作者群還有提到未來可能會推出JS版,有用JS的人可以期待一下。

*另一個library: SpriterMC有支援bones及IK,我在下一篇會簡單介紹一下SpriterMC



Index:
Spriter好好玩系列 (一) ---- 什麼是Spriter ?
Spriter好好玩系列 (二) ---- Spriter 基本操作
Spriter好好玩系列 (三) ---- 使用SpriterAS
Spriter好好玩系列 (四) ---- 使用SpriterMC

2013/3/1

Spriter好好玩系列 (二) ---- Spriter 基本操作


這篇來講基本操作。

首先到這個頁面去下載,Spriter 同時支援了win及mac,請找自己需要的版本去下載


目前免費提供的版本是alpha4.1版,未來正式版也會分free及pro付費版,free版會有一些功能限制,這個頁面有詳細說明,有興趣可以看看。

各位應該有注意到下載頁面右邊有放了兩支影片,上面還寫了"Don't try Spriter without watching these first"。對,沒錯,請務必看過這兩支影片再來試會比較快。因為目前還在alpha 版,作者幾乎沒有提供任何教學、範例、說明書之類的,而軟體本身介面也沒有簡單到像iPhone一樣可以不看說明書就會用,所以建議是先耐心看過影片後,再來使用。

以下我就挑一些重點來說明

.新建Project時會請你選擇一個folder,請直接選擇你放置動畫元件的folder,這些元件才會出現在你右邊的清單裏,然後就可以從右邊的清單拉進stage裏。(如下圖)


.如下圖,當游標在A區時,滑鼠滾輪可縮放stage;當游標在B區時,滾輪可縮放timeline;C區也可設定stage的縮放,但他會自動隱藏,游標移到A及C區時會出現。


.下圖為Timeline區。Spriter的Timeline是以ms為單位,這樣比較能通用於各種平台。圖中a處為playhead,拖動playhead至你要新增keyframe的地方,然後直接調整你的角色,keyframe就會自動產生。另外也可以在拖動playhead至某時間點後,直接按下Add Keyframe(圖中b點),便會以當下角色的狀態產生一個新的keyframe。最後c點的按鈕為"是否要將最後一個keyframe做tween回到第0ms的keyframe"。(也就是頭尾串接的動畫)

另外上圖有顯示"CurrentPlaybackTime:391",為目前playhead所在的詳細位置,你可以在"391"那個地方點擊滑鼠,會出現手動輸入框,可精準調整playhead位置。

.左邊是目前出現在stage上的元件列表,可drag移動上下層關係,愈下面的layer在Stage裏表示愈上層。

值得一提的是,Spriter可在不同Keyframe中變換Layer的順序,這在以往Flash上是十分麻煩的事情。

.點擊Stage上的元件都會出現如下圖的畫面,元件四週的九個方塊是形變控制點,而a點可控旋轉,另外b點是中心點,所有形變、旋轉都是以b為中心。如果想直接輸入數字來控制元件,可點擊c點開啟詳細面板。


.提到旋轉,就不能不知如何調整中心點位置。一般情況下,只要拖曳中心點的那個圓圈即可,但一個新拉進stage的元件,Spriter預設會把中心點設在左上角,與左上角的形變控制點重疊在一起,直接移動的話會變成是點到形變,而不是中心點,所以在點擊時要注意一下游標位置。

當游標移至重疊的中心點時,中心點的圓圈會放大以方便分辨,這時若點擊在綠色區域,則會移動中心點,若點擊在紅色區域,則做形變控制。

.Spriter也支援元件中途變換不同圖片的功能,只要在stage上以右鍵點擊元件,就會秀出小面板讓你切換。此變換只對目前的keyframe有作用,之前的keyframe還會是舊的那張圖。也就是"換圖"這件事也是keyframe包括的內容之一。


.再來是類似Flash中的Onion Skin功能,可以讓你看到前幾格或後幾格keyframe的位置,這是2D動畫中十分重要的功能。開啟後效果如下圖

紅色表示前幾格,綠色表示後幾格。開始方式請參考下圖紅、綠色塊的位置,以playhead為分界點,timeline線以上的這塊區域,在紅色範圍內按右鍵拖拉即可設定過去的keyframes顯示,而綠色範圍內右鍵拖拉即可設定未來的keyframes顯示


.在Timeline上也以一次選取多個Keyframes,下圖中,先點一下a,按著"shift"不放再點一下b,即可選取多個keyframes。選取後,可進行移動、複製貼上等功能,也可在選取多個的狀態下,按著"alt"不放,去拖拉a點或b點,即可做整串keyframe的壓縮及放大。


.Spriter也提供了Bones系統及IK功能,只要在stage上空白處,按著"alt"同時以滑鼠點擊拖拉即可拉出一個Bone。在選取某個Bone的狀況下新增Bone的話,則新的Bone則會是原來選取的Bone的Child。選取任何Bone時,都會以顏色提示其主從關係,如下圖


.另外也可以在左側的Bone Hierarchy面板調整Bone的主從關係,在這面板也可以為各個Bone命名,以及將元件與Bone綁定,如下圖,head_2.png即與bone_000綁定在一起。這個面板是以樹狀結構呈現,一切都用拖拉即可完成。最後要注意一點,這個面板會列出所有Bones及stage上的元件,如果沒看到元件,可能是右上角的眼睛沒點開。(Show Sprites in Heirarchy)


.在調整Bone時,一樣有方形的形變控制點及圓形的旋轉控制點,當按著"shift"同時調整旋轉控制點的話,即可開啟IK功能。而在Bone上按下右鍵的話,會出現IK的錨點功能,也就是這段Bone的終點會釘在某個位置,並牽制其他Bone的變動。


.完成一串動畫即可存成一則animation,右側下的Animations可檢視目前所有的animation,並可在此面板中新增、複製、刪除animation,一個SCML檔可存入多個animations


.最後當所有動作都完成時,選擇"File"->"Save Project"即可存檔(同時會產生scml檔),然後就可以拿去做程式開發了。

.Spriter目前還在alpha,所以還有很多bug,例如偶爾會無預警關閉,用IK時bone會往奇怪的角度旋轉、莫明產生keyframe…等,都不是什麼大問題,不過建議就是勤於存檔,並期待正式版時這些bug都清掉。若發現什麼bug想通報作者的話,可參考這篇的回報方式。



Index:
Spriter好好玩系列 (一) ---- 什麼是Spriter ?
Spriter好好玩系列 (二) ---- Spriter 基本操作
Spriter好好玩系列 (三) ---- 使用SpriterAS
Spriter好好玩系列 (四) ---- 使用SpriterMC

2013/2/28

Spriter好好玩系列 (一) ---- 什麼是Spriter ?


Spriter 是一套專門製作2D 遊戲動畫用的工具,官方自稱的副標是:
"The Ultimate 2D Game Animation Solution"
最近試玩幾天的心得是:假如他把一些小細節修得更完善的話,確實是有潛力揹起這樣的稱號。

Spriter 最早是出現在Kickstarter 上,並於2012/4 完成募資。可以先看一下他在Kickstarter上的介紹影片,之後再到他們的官網看看介紹及教學,還有目前版本狀況。
(本文撰寫時仍為alpha 4.1版,可免費下載,正式版預計2013 Q1 release)

簡單來說,目前2D遊戲的開發,不論是用什麼工具或程式語言,其動畫部份大多都會先請美術人員輸出一份SpriteSheet圖及描述檔。

(大概像這樣的東西)

上圖的動畫是用Flash繪製的,角色是由許多parts組成,然後在時間軸上設定多個keyframe,並於各個keyframe上將角色擺成不同的姿勢,最後才串成動畫

上圖動畫共使用了18個parts,拆解後的樣子如下圖:

利用這18個parts,我們可以擺出至少十多種不同的姿勢,但隨之而來的問題是,動作愈多最後產生的SpriteSheet檔就會愈大;而且不是只有關鍵姿勢而已,為了讓動畫效果流暢,姿勢與姿勢之間還會需要許多keyframes來串接。

以上面動畫的為例,從揮拳出去,再收拳回來到原點,一個簡單的動畫就用了8個keyframes來組成,如此不難想像一個完整的遊戲角色,全部動畫輸出的SpriteSheet檔後會有多大張,這些都會在程式開發階段,對記憶體管理及效能優化上造成負擔。

尤其近年來大家特別關注的手機、平板遊戲市場,在記憶體、CPU有限,但畫質又要與桌機品質相似(retina display)的狀況下,這種問題就格外需要注意。

而Spriter就是因應這樣的問題而生,他提供了一個像是陽春版的Flash介面,有timeline、keyframe這些東西。

(介面大致如上圖)

我們可以import多個png檔,當做組成角色的parts,然後在他的timeline上設計不同的動作並標示成keyframe,而keyframe完成後,中間的tween會自動產生。最後Spriter 會將所有keyframe的資訊輸出成一份SCML檔,我們只要將這份SCML檔連同那些parts圖檔,import到任何支援Spriter 格式的開發平台後,即可播放這些動畫。
(這邊可以查詢目前已支援Spriter的技術有哪些)

而SCML的內容其實只是XML,裏面是只記錄了每個keyframe的時間及各個parts所在的位置、尺寸、角度等資訊,如此即使再多的動作,也只是增加文字資訊,圖檔則只有一開始的那些parts而已。這種做法會比傳統的SpriteSheet省下許多的檔案大小負擔。

這種模式其實會讓人想起90年代末期,Flash崛起的過程。當時也是頻寬不足的狀況下,一般動畫格式不是GIF就是MPG,AVI等影片檔,但不論哪種,其原理都是連續圖檔播放,檔案都會很大,讓user難以下載。而Flash就是改以元件化的播放格式,大大減小了動畫檔的大小。而Spriter目前做的事情,便是類似的邏輯。
所以我個人認為:下一版的Flash應該要做出類似或直接支援SCML格式,不要再像CS6一樣,輸出了一堆SpriteSheet格式給別人用,然後自己一點符合時勢的創新都沒有。

最後,Spriter這套軟體目前還在alpha中,有許多UI操作上的小細節我個人覺得還不是很順手,期望他未來的版本可以更加成熟。



Index:
Spriter好好玩系列 (一) ---- 什麼是Spriter ?
Spriter好好玩系列 (二) ---- Spriter 基本操作
Spriter好好玩系列 (三) ---- 使用SpriterAS
Spriter好好玩系列 (四) ---- 使用SpriterMC