2013年1月31日 星期四

解決 Archive 的生成問題

Unable to Create Archive

上年替一個朋友開發他的遊戲項目,由於人手問題,哪個專案外包出去了。今日收到外包人員的源碼,要生成 IPA 給代理那邊做測試。可是在生成 IPA 時發現打包成功但沒有 Archive 輸出。搞了很久也沒有頭緒,後來找到解決方法。原來是Targets 內 Build Settings 的 Skip Install 設定了 Yes 便會有這樣的情況;把它改為 No 後便能成功輸出 Archive。

2013年1月30日 星期三

AMIGO Controller 支援日本語


《AMIGO Controller》原生只支援英文版,為了令日本同好也能使用,我決定加入日本語支援。在 iOS 環境下很易達到,只要把文字放進 Localizable.strings 就行了。

2013年1月29日 星期二

CircleView


為了能有效製作機體動作,在《AMIGO Controller》中加入了《Instant Pose Control》頁面。我希望使用者只要點著畫面上的機體關節進行拖拉,機體便能立即做出反應。這個控制方法在 iOS 下沒有類似的東西,因而自行開發了這個名為 CircleView 的東西。為了避免 Servo 因卡著而燒掉,在每個 CircleView 加入了最大及最小角度。只要在 Google Doc 或 .csv 內設定好,控制器便只會輸出有效角度來保護 Servo。以下是 CircleView 目前的代碼:
//
//  CircleView.h
//  AMIGOController
//
//  Created by Pacess HO on 10/1/13.
//  Copyright (c) 2013 Pacess HO. All rights reserved.
//

//--------------------------------------------------------------------------------------------------
//  0    1         2         3         4         5         6         7         8         9
//  567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890

#import 
#import 

//--------------------------------------------------------------------------------------------------
@class CircleView;

//==================================================================================================
@protocol CircleViewDelegate 

- (void)circleView:(CircleView *)circleView touchesBegan:(CGPoint)point;
- (void)circleView:(CircleView *)circleView touchesMoved:(CGPoint)point;
- (void)circleView:(CircleView *)circleView touchesEnded:(CGPoint)point;

@end

//==================================================================================================
@interface CircleView : UIView  {
 UILabel *_label;
 int _minValue;
 int _maxValue;
 int _value;
 id _delegate;

 CGFloat _lineWidth;

 CGPoint _startPoint;
 CGPoint _endPoint;
}

//--------------------------------------------------------------------------------------------------
- (id)initWithFrame:(CGRect)rect;
- (void)dealloc;

+ (Class)layerClass;
- (CABasicAnimation *)animationWithKeyPath:(NSString *)keyPath;

- (void)drawRect:(CGRect)rect;

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)updateTouches:(CGPoint)point;

- (void)setDelegate:(id)delegate;
- (void)setValue:(int)value;
- (void)setMinValue:(int)value;
- (void)setMaxValue:(int)value;
- (void)setLineWidth:(CGFloat)value;

@end
//
//  CircleView.m
//  AMIGOController
//
//  Created by Pacess HO on 10/1/13.
//  Copyright (c) 2013 Pacess HO. All rights reserved.
//

//--------------------------------------------------------------------------------------------------
//  0    1         2         3         4         5         6         7         8         9
//  567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890

#import "CircleView.h"

//--------------------------------------------------------------------------------------------------
#define CIRCLEVIEW_FONT_COLOR     [UIColor colorWithRed:0/255.0f green:143/255.0f blue:114/255.0f alpha:1.0f]

//==================================================================================================
@implementation CircleView

//--------------------------------------------------------------------------------------------------
- (id)initWithFrame:(CGRect)rect  {
 self = [super initWithFrame:rect];
 if (self == nil)  {return self;}
 
 //  Initialization code
 _value = 100;
 _lineWidth = 4.0f;
 
 int width = rect.size.width;
 int height = 20;
 int x = 0;
 int y = (rect.size.height-height)/2;
 
 UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(x, y, width, height)];
 [label setBackgroundColor:[UIColor clearColor]];
 [label setTextAlignment:NSTextAlignmentCenter];
 [label setTextColor:[UIColor whiteColor]];
 [label setText:[NSString stringWithFormat:@"%d", _value]];
 [self addSubview:label];
 [label release];
 _label = label;
 
 [self setBackgroundColor:[UIColor clearColor]];
 return self;
}

//--------------------------------------------------------------------------------------------------
- (void)dealloc  {
 [super dealloc];
}

//--------------------------------------------------------------------------------------------------
+ (Class)layerClass  {
 return [CAShapeLayer class];
}

//--------------------------------------------------------------------------------------------------
- (CABasicAnimation *)animationWithKeyPath:(NSString *)keyPath  {
 CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:keyPath];
 [animation setAutoreverses:YES];
 [animation setRepeatCount:HUGE_VALF];
 [animation setDuration:1];
 return animation;
}

//--------------------------------------------------------------------------------------------------
- (void)drawRect:(CGRect)rect  {
 CGContextRef context = UIGraphicsGetCurrentContext();

 CGFloat angle;
 CGFloat minAngle = ((_minValue)/180.0*M_PI);
 CGContextSetLineWidth(context, _lineWidth);

 int x = self.frame.size.width/2;
 int y = self.frame.size.height/2;

 CGContextSaveGState(context);
 CGContextAddArc(context, x, y, x, 0, M_PI*2.0f, 0);
 if (_startPoint.x == 0 && _startPoint.y == 0)  {

  //  Not selected
  CGContextSetRGBFillColor(context, 0/255.0f, 57/255.0f, 71/255.0f, 1.0f);
 }  else  {

  //  Selecting
  CGContextSetRGBFillColor(context, 0/255.0f, 143/255.0f, 114/255.0f, 1.0f);
 }
 CGContextDrawPath(context, kCGPathFill);
 CGContextRestoreGState(context);

 //--------------------------------------------------------------------------------------------------
 //  X, Y, Radius, Start angle, End angle, Clockwise
 CGContextSaveGState(context);

 angle = ((_maxValue)/180.0*M_PI);
 CGContextAddArc(context, x, y, x-_lineWidth, minAngle, angle, 0);
 CGContextSetRGBStrokeColor(context, 1.0f, 1.0f, 1.0f, 1.0f);
 CGContextDrawPath(context, kCGPathStroke);

 CGContextRestoreGState(context);

 //--------------------------------------------------------------------------------------------------
 //  X, Y, Radius, Start angle, End angle, Clockwise
 angle = ((_value)/180.0*M_PI);
 CGContextAddArc(context, x, y, x-_lineWidth, minAngle, angle, 0);
 CGContextSetRGBStrokeColor(context, 1.0f, 0.0f, 0.0f, 1.0f);
 CGContextDrawPath(context, kCGPathStroke);
}

//--------------------------------------------------------------------------------------------------
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event  {
 UITouch *touch = [touches anyObject];
 _startPoint = [touch locationInView:self];
 _endPoint = _startPoint;
 [self setNeedsDisplay];
 
 [_delegate circleView:self touchesBegan:_startPoint];
}

//--------------------------------------------------------------------------------------------------
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event  {
 UITouch *touch = [touches anyObject];
 CGPoint currentPoint = [touch locationInView:self];
 [self updateTouches:currentPoint];
 
 [_delegate circleView:self touchesMoved:currentPoint];
}

//--------------------------------------------------------------------------------------------------
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event  {
 _startPoint = CGPointMake(0, 0);

 UITouch *touch = [touches anyObject];
 CGPoint currentPoint = [touch locationInView:self];
 [self updateTouches:currentPoint];
 
 [_delegate circleView:self touchesEnded:currentPoint];
}

//--------------------------------------------------------------------------------------------------
- (void)updateTouches:(CGPoint)point  {
 CGFloat x = point.x-_endPoint.x;
 _endPoint = point;
 
 if (x > 0.0f)  {
  
  //  Move right
  _value += x;
 }  else  {
  
  //  Move left
  _value += x;
 }
 
 [self setValue:_value];
 [self setNeedsDisplay];
}

//--------------------------------------------------------------------------------------------------
- (void)setDelegate:(id)delegate  {
 _delegate = delegate;
}

//--------------------------------------------------------------------------------------------------
- (void)setValue:(int)value  {
 _value = value;
 if (_value < _minValue)  {_value = _minValue;}
 if (_value > _maxValue)  {_value = _maxValue;}

 [_label setText:[NSString stringWithFormat:@"%d", _value]];
}

//--------------------------------------------------------------------------------------------------
- (void)setMinValue:(int)value  {
 _minValue = value;
 [self setValue:_value];
}

//--------------------------------------------------------------------------------------------------
- (void)setMaxValue:(int)value  {
 _maxValue = value;
 [self setValue:_value];
}

//--------------------------------------------------------------------------------------------------
- (void)setLineWidth:(CGFloat)value  {
 _lineWidth = value;
}

@end

2013年1月28日 星期一

藍牙 4.0 模塊


藍牙 4.0 模塊 BLE mini 已經到手。利用 Xcode 及 iOS SDK 製作的 AMIGO Controller 加入了相關的連線功能,已能成功連接及收發數據。把 BLE mini 接上示波器能顯示出接收到的訊號。十分易用的一塊板子。現在機體的開發進度己經七七八八,是時候加大馬力開發 AMIGO Controller,希望能在 5 月前完成新的二足步行機體。

2013年1月23日 星期三

設計 vs 意見


每次有應徵者來面見,都會給他一台 iPad。內裡有一個《職位申請表》的應用,給應徵者填上個人資料、履歷、期望薪酬等內容。還有拍照功能,單憑文字不能喚醒記憶,相片能幫助我們記起應徵者是個怎樣的人。

今天有應徵者使用時出了狀況,卡在填寫身份證號碼一欄。這個應用還有改進的地方,尤其是大多數人都不知道需要拍照;但身份證號碼那個位置自問十分清楚明確,明明是要填英文字母及首三位數字,卻填上了後半段。這個應用不論是技術層面、外觀層面、或是使用層面都下過功夫。使用過的人也有 26 人,只是今次才出現問題。從數值上來說,比例很低。唯一能解釋的是應徵者看漏眼或不小心。問題是值得為優化應用,從而令這類人也不會卡住嗎?如果是介面問題,我認為是值得的;如果是人為問題,則要視乎會否破壞設計的觀感。

這事件換來了我跟另一位股東關於「設計 vs 意見」的爭論。就好像 iPhone 4 的設計,十分優雅美觀,卻換來天線接收力減弱的問題。若我是 Steve Jobs,也一樣選用這個設計。對我來說,功能與外觀也都要是最好的。如果為了功能而破壞了設計,我會選擇犧牲功能。股東問我:「要是公司的作品受到用戶批評時,你會如何處理?」。我說:「視乎比例而定。若過半數人持相同意見時,就應該要改善;要是只有一兩個意見,那就不會改動;看情況而定。數據比較客觀。畢竟有時意見是主觀的,你有你喜歡,我有我鍾意,沒有一部通書睇到老的方法」。正如 iPhone 4 有天線門也能成為最暢銷手機;Galaxy Note 那麼硬膠也能受使用者歡迎一樣。一定有人喜歡,有人討厭,只是決定於比例而矣。

2013年1月22日 星期二

無奸不成商?

古語有云:「無奸不成商」。我不知道是不是定律,但至少在工作過的公司,除了 Lakoo 之外,都喜歡把好的數字打個十一折。甚麼是好的數字?就是業績、營利、用戶數目、員工數目…等。我不認為這樣做是好事,至少長遠來說不是。我喜歡明碼實價,你喜歡的就幫襯,不喜歡亦無妨,他日再合作。起碼我對得住人,亦能驘來一份信任。我想這是為甚麼不用跑街,客人也會找上我的原因。雖然報大數客人未必能即時發現,但要是發現起來時則十分尷尬。我不想處理這樣的情況,還是老老實實做人好。可能我不適合營商,也可能是程序員出身的我只有 0 及 1 的關係,不是對就是錯。

2013年1月21日 星期一

十人應戰

己經是連續第三個星期有新同事加入。開發團隊一個月內增加至十人,能力得到提升,可以開發更多優質應用。上年一直卡在請人的關口;不是沒有人應徵,就是條件不足。經驗告訴我,有時真的要看時機。最近一個月的應徵者質素都比以往的好,這也是改用了其他請人方法,而得到新的結果吧!

2013年1月18日 星期五

開齋

得到朋友的介紹與支持,2013 年度第一單生意終於落實了。在公司而言,又多了一位能長期合作的新客戶。這是一單不小的生意,也讓我有機會開發 Windows RT 應用程式,提升公司的競爭力。多謝朋友們對我的認同及支持,公司才能有今日的成長!我們會努力開發更多優質的應用!

有幸今日認識兩家公司,在介紹自己公司的作品後,得到正面的評價。客人更指我們的作品有創意。沒錯,我的確花了很多時間在構思新點子,也嘗試不同的技術,更像 Steve Jobs 般把軟件硬件合一,創造不同的體驗。這是我認為能跟別人競爭的特色。客人能感受到這點,實在滿足。

2013年1月17日 星期四

古惑仔 3G


還記得十多年前提議天宇科技老闆阿喬開發第一代《古惑仔 Online》,當時阿喬對題材極有興趣,但也十分擔心;怕古惑仔的負面形象會教壞小朋友,影響公司形象。為了減輕阿喬的疑慮,還特別加入可扮演警察角色;阿喬才願意開發遊戲。轉眼間,遊戲已經發展到手機上的《古惑仔 3G》;而且還由老僱主 Gameone 操刀開發,實在懷念。

2013年1月16日 星期三

Reverse Engineering


第一次的《拉闊圖書館》已經順利舉行,大家對改 Game 拆 Game 興緻勃勃。同事們學到關於《逆向工程》的一些技巧。希望他們能在工作中實現出來,提升安全及競爭力。當中用到的《PixelViewer》源碼已放到 Google Code,有興趣的朋友可到這裡下載

第二次的《拉闊圖書館》將會由同事分享,期待學到新的知識!

2013年1月15日 星期二

BLE mini



今日收到了 BLE mini 的資料。看過它示範程序的源碼,發現十分簡單易用。這塊低功率藍牙模塊將會用在我新的機體上。十分期望能盡快完成!

2013年1月14日 星期一

PixelViewer.二



最近發現不論是 3D 的還是 2D 的遊戲,很多都用上 Unity 來開發。它有一個特點,就是所有圖案、音效、數據、模型...等都打包到 .assets 檔。用《HexEdit》打開它全是凌亂的資料,意味著不是壓縮了,就是加密了。要破解實在很花功夫及時間。既然開發了《PixelViewer》,就拿它來打開 .assets 檔,得到了意外的收獲。

原來部份貼圖沒有壓縮或加密,只要設定正確,便能用《PixelViewer》打開。有趣的是,貼圖都是上下倒轉。然而《PixelViewer》內包含旋轉功能,可以把圖案 180 度旋轉。不過,原來圖案也是左右倒轉了。這樣做的原因,相信是因為 3D 環境下的座標系統是以左下角為 (0, 0);而不是畫面左上角為 (0, 0) 的緣故。

2013年1月11日 星期五

PixelViewer




網友 Kito 希望我能分享分析圖檔的心得,碰巧同事想破解的《Puzzle & Dragons》正屬此類作品,所以把它加進《Reverse Engineering》Keynote 內。

要顯示《Puzzle & Dragons》的圖檔,部份可以用 Photoshop 來達成;但某些則要編寫程序。我嘗試利用 Xcode 開發出一個在 Mountain Lion 運作的工具,名為《PixelViewer》。目前已取得成果,之前的記憶體導致死機的問題也得到解決。這個工具算是完成了!有興趣使用的朋友可按這裡下載。僅支援 Mac OS X Mountain Lion。

2013年1月9日 星期三

初創事業的難題

公司預計的美術部空缺已經填滿了,理應不再面見新的應徵者。不過,若遇到有能力的應徵者時,我還是會進行面見。要是乎合工作要求及處事態度的話,會備案作為他日之用,甚至會考慮聘請成為同事。

然而,這個時間來了。最近的一位美術設計師達到了這個要求,是難得的人才。如果留作日後之用,相信他已經找到工作,不會考慮到我公司幫手。可是,要聘請的話,考慮的事情也很多。

如公司座位問題,開張時打算容納六位員工的空間,現時已塞了八人。業務部也在招聘,再加入多一個美術人員,空間將會很擠迫。

如工作問題,原先預計的工作量足夠三位美術人員消化;四位美術人員的話,則要把自家應用的美術含量提升。

如收支問題。人手多了,需要接更多的工作才可以支付多出來的開支。本來對今年的收入目標已有一定壓力,現在就更加重了。

對於初創公司,資源相對緊張的情況下,每一步都要小心為營,才能避免對公司帶來創傷。2013 年為癸巳蛇年,我的桃花值得到提升,今年能接到的工作一定比 2012 年多,不過會在下半年才會發生...。那就要努力捱過上半年了!

2013年1月8日 星期二

初音ミク


下星期即將舉行第一次的《拉闊圖書館》,正準備當日播放的 Keynote。看到 Line 加入了初音的貼紙。相信很多同事都想知道取得圖片的方法,所以決定加進《Reverse Engineering》教學內。希望大家能得到一點啟發。

現在只欠 Flash 逆向題材還沒準備好,希望能在餘下的時間內完成吧!

2013年1月7日 星期一

LINE Pop 指導器(Mac)

LINE Pop Trainer (Mac)


最近愛上玩《LINE Pop》這個遊戲。喜歡它的簡單及流暢,編程做得很好。我的分數大約在 12 萬分左右,順手時則是 22 萬分。相較於朋友們的 68 萬分,實在相距甚遠。很想能把分數拉近一點。加上心血來潮,決定利用編程能力來幫自己一把。

iPhone 無法同時運行兩個應用,所以不能在實機上進行。在沒有 Jailbreak 的情況下也無法透過 VNC 控制 iPhone,否則只要在電腦上運算後直接進行遊戲。唯有利用 AirPlay 把畫面傳到電腦,進行分析後再顯示能撥動的方塊及方向。要達到以上目的,需要用上 Reflection 軟件。它能讓 Mac 機透過 AirPlay 顯示 iOS 畫面。鏡像模式只支援 iPhone 4S 或以上機種。

有了遊戲畫面後,下一步便是編寫程式選取方塊的種類,然後分析步法。在 Apple 網找到一個名為 Son of Grab 的抓圖示範程式。它列出了當前運行的程式。點選程式名字便能抓取應用的畫面,十分切合今次之用。抓取畫面成為 NSImage 後,就要認出各個方塊的種類。


這類遊戲在設計上,方塊的顏色不會太接近,所以拿顏色來分析即可。由於方塊是以一格格排列好,程式只要指定每格讀取顏色的位置便可,7x7 = 49 次,不用整個畫面讀取。由於 RGB 色彩空間不好分辨,我需要把它轉換為 HSL 色彩空間。之後只要檢查 Hue 值及 Lightness 值便能準確分辨顏色。上圖的左下角便是分辨出來的結果。把顏色分類為 1-7 後再進行分析;0 是無法辨認。


目前沒有想到快捷方便的分析方法,只把所有組合 Hard code 好,程式已能正常運作。我在適合移動的方塊旁加入粗紅色邊,指示要拖動的位置。如左邊為紅色則向左移;右邊為紅色則向右拉。整個程式花了大約四個小時製作,能正確分析。得到這個指導器的幫忙,我的分數的確高了,但卻仍然停留在 20 萬分以下。可能是一邊望著電腦一邊對著手機,都花了不少時間。實際上幫忙不大,但今次的開發是一個不錯的實驗!

2013年1月6日 星期日

在 Mac 上顯示網絡中所有的設備

IP Scanner for Mac

家中有很多上網設備,大概也有 15 台。它們大多沒有指定 IP 地址,有時想透過 IP 連接的話,需要左找右查,不太方便。在 Mac App Store 發現一個名為「IP Scanner」的免費應用,能顯示網絡中的所有設備,十分好用。

2013年1月5日 星期六

在 Mac 上把 DVD 聲軌轉為 MP3

Extract Soundtracks From DVD (Mac)

早前買了《Concert YY》DVD 給太太,當中的歌曲很有意思,太太時常在看。我在工作時需要很專心,極少聽音樂;不過,有時也需要點外來刺激。既然買了 DVD,把它的聲軌轉為 MP3 在工作時聽也不錯。

在 Mountain Lion 上把 DVD 轉成 MP3 不難,工具也是免費。先下載 HandBrake,安裝並執行。在 Video 頁選 H.264 (x264),Constant Quality 選最低。在 Audio 頁選 Stereo 聲道。點 Start 開始把 DVD 轉成 .m4v 影片檔。

完成後用 ExtractMovieSoundtrack 讀取剛輸出的 .m4v 檔案。記得打勾 Divide by Chapters,這樣輸出的 AIFF 檔才會按 DVD 的聲軌分拆。完成後用 iTunes 把 .aiff 轉換成 .mp3。

2013年1月4日 星期五

認色

Color Detection

下星期將會出席客人的創意會議,我構思了一個認顏色的示範程序。原本是以 RGB 值來判斷出是紅、橙、黃、綠、藍、紫哪個顏色,但發現誤差頗大,所以改為另一個方法。結果十分滿意。

我順便把這個「認色」問題丟給程式員同事們,給他們兩天時間思考。在星期一給我解決方法,測試一下他們的解難能力。

2013年1月3日 星期四

放大圖片時的 CSS 技巧


在《little diary》上遇到一個問題。客人希望贈劵圖片能在放大後仍然清楚,所以新的圖片使用了長闊大一倍的比例;亦即是面積比往日大了三倍的圖片。贈券畫面是使用 UIWebView 來達成,貪它能放大縮小、能跑程序、能加入圖片文字甚至是聲音影片。能滿足客人的需要,又能透過服務器更新,更能擁有修改的彈性。由於贈券圖片解像度倍增,需要調用 Viewport 值才能顯示整張贈券。總不能要用戶手動縮放這樣麻煩。可是問題來了。原本贈券以外的按鈕圖片都縮小了,需要稍作處理。方法有二:

1. 加大按鈕圖片解像度
2. 利用 CSS 放大

前者需要重製圖片,也要從服務器把圖片推到手機內,當中也有可能影響到之前標清的贈券(假如按鈕圖檔沒有改名)。後者則簡單獨立地完成,工序及影響較細。我選擇了後者。

原本以為在 CSS 中加入 img {width: 200%;} 就能搞定,誰知因為「下載」按鈕是靠右泊邊的關係,圖片是文大了,但一半圖案超出了畫面。估計是靠邊時以圖片原大尺寸計算。後來找到解決方法。要把 CSS 改為 img {width: 200%; float: right;},才能顯示成心目中想要的效果。

2013年1月2日 星期三

Samsung 用戶

在 Samsung Apps 推出的《B.Duck Camera》已有一段時間。當中收到很多用戶的投訴,指在啟動時有問題、拍攝後會彈機、系統升級後不穩定、付費後沒有更新內容…等。作為一個負責任的開發商,我們第一時間在 Galaxy SII, Galaxy SIII, Galaxy Note II LTE, Galaxy Nexus上測試,也在程式層面上尋找有問題的地方。可是都找不到出現問題的地方。就算找 Samsung 的技術人員在實機上測試也沒有出現問題。能解決用戶的問題,只能通過電話直接詢問與測試。可是當回覆用戶時卻得到「銷售商,回應時切勿 copy and paste,請真心回覆,不同人的問題都是得到相同的 feedback……差勁!!!」的回應,實在氣憤。

在實機上測試好沒有問題的應用,到上架後客人說有問題,我們立即進行測試,找不到問題,需要電話跟進協助。回答當然是千篇一律的「請致電 21857695,我們會提供協助」,還有甚麼可說呢?況且就算 Copy & Paste 也算是有心回覆,總好過甚麼都不回吧!而且還提供電話一對一協助,連 Apple 都做不到這樣的服務!不過,向好的方向想,假如能取得這位刁難用戶的滿意,亦是一個進步。下次會以「XXX,我們明白你的情況,實在抱歉。麻煩請致電 21857695,我們會提供協助」回覆,希望好一點。