RPG(ドラクエふう)のマップをFlashで作成する。
マップ部品は「First Seed Material」http://www.tekepon.net/fsmの提供フリー素材「マップチップ」「キャラクターチップ」を使用させていただく。
注意書き
「このソフトは、REFMAPが配布しているフリー画像素材を使用してます。このソフト内で使用されている画像を、このゲームを遊ぶ以外の用途には使用しないで下さい」
しかしこのマップチップは「RPGツクール2000」で使うのが主な用途で、それ以外の用途で使うには工夫が必要だ。私はツクール使わずFlashでRPG作るつもりなので、
16px x 16px のチップを並べて作られた1枚のBMP画像を、使いやすくしてみる。
透過
透過設定がしてないので、透過処理をする
- Photoshopで読み込み
- colorModeをRGBに変更
- layer名をdoubleClickして、Layer名を'Background'から'Layer 0'に変更。
- 透過色をMagicWandで選択。そのとき Contiguous(隣接) オプションのチェックを外せば、透過色をすべて選択できる。
- 削除
- png形式で保存。PNG-8でも、PNG-24(alpha含めると実質PNG-32)でもよい。
グリッド線をひいて、番号をつけておく
Photoshopの自動化スクリプトJSXを使い、16pxごとにグリッド線をひいて、番号を表示する。この画像を見ながらチップを配置する。
line16x16px.jsx
//線の太さ lineWidth = 1; //線の間隔 pxBetweenX = 16; pxBetweenY = 16; //foreground color: 以下のdrawLine()で使う線の色 theColor = new SolidColor() theColor.rgb.hexValue = 'FFFFFF'; cTID = function(s){ return charIDToTypeID(s); }; sTID = function(s){ return stringIDToTypeID(s); }; //線を引く関数はコピペさせていただいた //http://forums.adobe.com/thread/803261 function drawLine( startXY, endXY, width ) { var desc = new ActionDescriptor(); var lineDesc = new ActionDescriptor(); var startDesc = new ActionDescriptor(); startDesc.putUnitDouble( cTID('Hrzn'), cTID('#Pxl'), startXY[0] ); startDesc.putUnitDouble( cTID('Vrtc'), cTID('#Pxl'), startXY[1] ); lineDesc.putObject( cTID('Strt'), cTID('Pnt '), startDesc ); var endDesc = new ActionDescriptor(); endDesc.putUnitDouble( cTID('Hrzn'), cTID('#Pxl'), endXY[0] ); endDesc.putUnitDouble( cTID('Vrtc'), cTID('#Pxl'), endXY[1] ); lineDesc.putObject( cTID('End '), cTID('Pnt '), endDesc ); lineDesc.putUnitDouble( cTID('Wdth'), cTID('#Pxl'), width ); desc.putObject( cTID('Shp '), cTID('Ln '), lineDesc ); //desc.putBoolean( cTID('AntA'), true ); desc.putBoolean( cTID('AntA'), false ); executeAction( cTID('Draw'), desc, DialogModes.NO ); }; // 幅・高さを調べる //perferences.rulerUnits = Units.PIXELS; width = activeDocument.width; height = activeDocument.height; // color mode変更 activeDocument.changeMode(ChangeMode.RGB) // layer追加 theLayerSet = activeDocument.layerSets.add() theLayer = theLayerSet.artLayers.add() // foreground color指定 app.foregroundColor = theColor; // 線を引く y = 0; for (i=0; i<height; i+=pxBetweenY){ drawLine([0,y], [width,y], lineWidth); y += pxBetweenY; } x = 0; for (i=0; i<width; i+=pxBetweenX){ drawLine([x,0], [x,height], lineWidth); x += pxBetweenX; } // 文字を置く function putText(num, x, y){ theLayer = theLayerSet.artLayers.add(); theLayer.kind = LayerKind.TEXT; theLayer.textItem.font = 'Courier New'; theLayer.textItem.size = 9; theLayer.textItem.color = theColor; theLayer.textItem.antiAliasMethod = AntiAlias.NONE; theLayer.textItem.position = [x+7, y+9]; str = '0123456789abcdefghijklmnopqrstuvwxyz'.charAt(num); theLayer.textItem.contents = str; theLayer.rasterize(RasterizeType.TEXTCONTENTS); if(num!=0) theLayer.merge(); }; for (i=0; i<height/pxBetweenY; i++){ putText(i, 0, i * pxBetweenY); } for (i=1; i<width/pxBetweenX; i++){ putText(i, i * pxBetweenX, 0); }
Flash
16x16 pxの大きさは表示するには小さいので、3倍の 48x48px に拡大して貼ることにしたい。
Flash ActionScript3.0のcopyPixels()は等倍コピーなので、拡大するにはdraw()で拡大したBitmapDataを用意してからcopyPixels()すればよいようだ。
参考
- Flashの描画速度をBitmapDataクラスを使って上げる方法 http://sipo.jp/blog/2009/12/flashbitmapdata.html
- [AS3]copyPixelsのお話 http://rairaraihome.sblo.jp/article/29947646.html
この画像をswfにEmbed(埋め込み)して、表示する。
package { import flash.display.Bitmap; import flash.display.BitmapData; import flash.display.PixelSnapping; import flash.display.Sprite; import flash.events.Event; import flash.geom.Matrix; import flash.geom.Point; import flash.geom.Rectangle; /** * ... * @author itouhiro */ [SWF(width="480",height="480",backgroundColor="0x000000",frameRate="60")] public class Main extends Sprite { //[Embed(source = "tekepon.net-fsm-road02_a.png", mimeType="image/png")] //←これでもよい [Embed(source = "tekepon.net-fsm-road02_a.png")] private var imgSrc:Class; public function Main():void { if (stage) init(); else addEventListener(Event.ADDED_TO_STAGE, init); } private function init(e:Event = null):void { removeEventListener(Event.ADDED_TO_STAGE, init); // entry point var mapTipOriginal:Bitmap = new imgSrc(); //BitmapDataの第4引数を0x00000000にしておかないと、copyPixels(,,,null,null,true)のときalpha部分が透過しないで白になる // ref. http://www.kongregate.com/forums/4-game-programming/topics/250811 var mapTip:BitmapData = new BitmapData(mapTipOriginal.width * 3, mapTipOriginal.height * 3, true, 0x00000000); //3倍 (16px→48px)に拡大してcopy var mtx:Matrix = new Matrix(); mtx.scale(3, 3); mapTip.draw(mapTipOriginal, mtx); //背景用BitmapDataにマップチップの「草原」部分をcopy var bgSrc:BitmapData = new BitmapData(480, 480, false, 0x888888); var x:int; var y:int; for (y = 0; y < mapTip.height; y+=48) { for (x = 0; x < mapTip.width; x+=48) { bgSrc.copyPixels(mapTip, new Rectangle(0,8*48,48,48), new Point(x, y)); } } //「大樹」を配置します bgSrc.copyPixels( mapTip, new Rectangle(26 * 48, 11 * 48, 4 * 48, 5 * 48), new Point(2 * 48, 3 * 48), null, null, true); //BitmapDataを表示するためには、Bitmapが必要 var bg:Bitmap = new Bitmap(bgSrc); addChild(bg); } } }
キャラ
キャラチップは 24x32px なのか‥‥。
先ほどのjsxを少し変更して、24x32でグリッドひいて数字表示。
//線の太さ lineWidth = 1; //線の間隔 pxBetweenX = 24; pxBetweenY = 32; //foreground color: 以下のdrawLine()で使う線の色 theColor = new SolidColor() theColor.rgb.red = 0; theColor.rgb.green = 0; theColor.rgb.blue = 0; cTID = function(s){ return charIDToTypeID(s); }; sTID = function(s){ return stringIDToTypeID(s); }; //線を引く関数はコピペさせていただいた //http://forums.adobe.com/thread/803261 function drawLine( startXY, endXY, width ) { var desc = new ActionDescriptor(); var lineDesc = new ActionDescriptor(); var startDesc = new ActionDescriptor(); startDesc.putUnitDouble( cTID('Hrzn'), cTID('#Pxl'), startXY[0] ); startDesc.putUnitDouble( cTID('Vrtc'), cTID('#Pxl'), startXY[1] ); lineDesc.putObject( cTID('Strt'), cTID('Pnt '), startDesc ); var endDesc = new ActionDescriptor(); endDesc.putUnitDouble( cTID('Hrzn'), cTID('#Pxl'), endXY[0] ); endDesc.putUnitDouble( cTID('Vrtc'), cTID('#Pxl'), endXY[1] ); lineDesc.putObject( cTID('End '), cTID('Pnt '), endDesc ); lineDesc.putUnitDouble( cTID('Wdth'), cTID('#Pxl'), width ); desc.putObject( cTID('Shp '), cTID('Ln '), lineDesc ); //desc.putBoolean( cTID('AntA'), true ); desc.putBoolean( cTID('AntA'), false ); executeAction( cTID('Draw'), desc, DialogModes.NO ); }; // [効果 - 光彩]の関数もコピペさせていただいた // http://stackoverflow.com/questions/7696212/how-do-you-modify-blending-options-with-a-photoshop-script function addStyleGlow( R, G, B, blendingMode, opacity, spread, size ){ var desc1 = new ActionDescriptor(); var ref1 = new ActionReference(); ref1.putProperty(cTID('Prpr'), cTID('Lefx')); ref1.putEnumerated(cTID('Lyr '), cTID('Ordn'), cTID('Trgt')); desc1.putReference(cTID('null'), ref1); var desc2 = new ActionDescriptor(); desc2.putUnitDouble(cTID('Scl '), cTID('#Prc'), 100); // Glow color var desc4 = new ActionDescriptor(); var rgb = new Array(); desc4.putDouble(cTID('Rd '), R); desc4.putDouble(cTID('Grn '), G); desc4.putDouble(cTID('Bl '), B); // Blending mode of the effect var desc3 = new ActionDescriptor(); desc3.putBoolean(cTID('enab'), true); desc3.putEnumerated( cTID('Md '), cTID('BlnM'), cTID(blendingMode) ); desc3.putObject(cTID('Clr '), sTID("RGBColor"), desc4); // Opacity desc3.putUnitDouble(cTID('Opct'), cTID('#Prc'), opacity); desc3.putEnumerated(cTID('GlwT'), cTID('BETE'), cTID('SfBL')); // Spread desc3.putUnitDouble(cTID('Ckmt'), cTID('#Pxl'), spread); // Size desc3.putUnitDouble(cTID('blur'), cTID('#Pxl'), size); // Noise desc3.putUnitDouble(cTID('Nose'), cTID('#Prc'), 0); // Quality: Jitter desc3.putUnitDouble(cTID('ShdN'), cTID('#Prc'), 0); desc3.putBoolean(cTID('AntA'), true); var desc5 = new ActionDescriptor(); desc5.putString(cTID('Nm '), "Linear"); //desc5..putString(cTID('Nm '), "\x90\xFC\x8C\x60" ); //Shift-JIS '線形' desc3.putObject(cTID('TrnS'), cTID('ShpC'), desc5); // Quality: Range desc3.putUnitDouble(cTID('Inpr'), cTID('#Prc'), 50); desc2.putObject(cTID('OrGl'), cTID('OrGl'), desc3); desc1.putObject(cTID('T '), cTID('Lefx'), desc2); executeAction(cTID('setd'), desc1, DialogModes.NO); }; // 幅・高さを調べる //perferences.rulerUnits = Units.PIXELS; width = activeDocument.width; height = activeDocument.height; // color mode変更 activeDocument.changeMode(ChangeMode.RGB) // layer追加 theLayerSet = activeDocument.layerSets.add() theLayer = theLayerSet.artLayers.add() // foreground color指定 app.foregroundColor = theColor; // 線を引く y = 0; for (i=0; i<height; i+=pxBetweenY){ drawLine([0,y], [width,y], lineWidth); y += pxBetweenY; } x = 0; for (i=0; i<width; i+=pxBetweenX){ drawLine([x,0], [x,height], lineWidth); x += pxBetweenX; } // 文字を置く function putText(num, x, y){ theLayer = theLayerSet.artLayers.add(); theLayer.kind = LayerKind.TEXT; theLayer.textItem.font = 'Courier New'; theLayer.textItem.size = 9; theLayer.textItem.color = theColor; theLayer.textItem.position = [x, y]; str = ('0' + num).slice(-2); theLayer.textItem.contents = str; theLayer.rasterize(RasterizeType.TEXTCONTENTS); if(num!=0) theLayer.merge(); }; for (i=0; i<height/pxBetweenY; i++){ putText(i, 4, i * pxBetweenY + 9); } for (i=1; i<width/pxBetweenX; i++){ putText(i, i * pxBetweenX + 4, 9); } addStyleGlow(255, 255, 255, 'Nrml', 100, 25, 5);
日本語PhotoshopCS6 (Locales\ja_JP\Support Files\tw10428.dat をrenameしてない場合)では最後の「光彩(外側)」(outer glow)がエラーになるが、別に問題はない。数字が見にくいので文字を縁取りしてるだけ。そのレイヤーを手動で「光彩(外側)」すればいい。
キャラをFlashに配置
マップチップは36進数(0-9a-z) 2桁で表すことにする。
キャラも配置してみた。
実際のFlash。一度クリックすると、カーソルキーで操作できる。当たり判定や重ね順はまだ作ってないので正しくない。
ソース
Main.as
package { import flash.display.Bitmap; import flash.display.BitmapData; import flash.display.PixelSnapping; import flash.display.Sprite; import flash.events.Event; import flash.events.KeyboardEvent; import flash.geom.Matrix; import flash.geom.Point; import flash.geom.Rectangle; import flash.ui.Keyboard; /** * ... * @author itouhiro */ [SWF(width="480",height="480",backgroundColor="0x1059FE",frameRate="30")] public class Main extends Sprite { [Embed(source = "tekepon.net-fsm-road02_a.png")] private var MapSrcFile:Class; [Embed(source = "tekepon.net-fsm-chara01_a.png")] private var CharaSrcFile:Class; private var chara:Bitmap; private var charaTip:Array; private var count:int = 0; private var index:int = 0; public function Main():void { if (stage) init(); else addEventListener(Event.ADDED_TO_STAGE, init); } private function init(e:Event = null):void { removeEventListener(Event.ADDED_TO_STAGE, init); // entry point var mapSrcOriginal:Bitmap = new MapSrcFile(); //BitmapDataの第4引数を0x00000000にしておかないと、copyPixels(,,,null,null,true)のときalpha部分が透過しないで白になる // ref. http://www.kongregate.com/forums/4-game-programming/topics/250811 var mapSrc:BitmapData = new BitmapData(mapSrcOriginal.width * 2, mapSrcOriginal.height * 2, true, 0x00000000); //2倍 (16px→32px)に拡大してcopy var mtx:Matrix = new Matrix(); mtx.scale(2, 2); mapSrc.draw(mapSrcOriginal, mtx); //背景用BitmapDataにマップチップをmapping var bgMap:Array = []; bgMap.push('-- -- -- -- -- -- -- -- f2 08 68 08 08 0c 1e'.split(' ')); bgMap.push('-- -- -- -- -- -- l8 m8 c3 08 08 08 08 08 0f'.split(' ')); bgMap.push('-- -- -- l8 m8 m8 08 08 c4 c3 08 08 08 g3 g3'.split(' ')); bgMap.push('q8 r8 s8 f2 08 08 08 08 c4 c4 c2 08 e2 h7 d4'.split(' ')); bgMap.push('-- -- -- f2 08 69 79 89 c5 c4 c2 08 e2 h7 d4'.split(' ')); bgMap.push('-- -- -- f2 08 6b 7b 8b 08 c5 c2 08 e2 eb 08'.split(' ')); bgMap.push('l8 m8 m8 f3 08 08 08 69 08 08 08 08 65 75 75'.split(' ')); bgMap.push('f2 08 08 c4 f3 d3 d3 08 08 08 08 08 67 76 75'.split(' ')); bgMap.push('f2 08 08 c5 c4 d4 d4 f3 08 08 08 08 08 67 77'.split(' ')); bgMap.push('f2 08 08 08 c5 d5 d5 c4 f2 08 08 e1 d3 d3 d3'.split(' ')); bgMap.push('f2 08 08 08 60 08 08 c4 c2 08 08 h2 h7 d4 d4'.split(' ')); bgMap.push('f2 l6 e1 h6 08 08 08 c5 c2 08 08 h2 h7 d4 d4'.split(' ')); bgMap.push('f2 m6 e2 h7 c3 08 08 08 08 08 08 08 eb 08 08'.split(' ')); bgMap.push('f2 m6 e2 h7 c4 c3 08 08 08 08 08 08 08 08 08'.split(' ')); bgMap.push('f2 m7 e2 h7 c4 c4 c3 08 08 08 08 08 08 08 08'.split(' ')); var bgBoard:BitmapData = new BitmapData(480, 480, true, 0x00000000); var i:int; var j:int; var str36:String = '0123456789abcdefghijklmnopqrstuvwxyz'; var sz:int = 32; //tip size var idxX:int; var idxY:int; for (i = 0; i < bgMap.length; i++) { for (j = 0; j < bgMap[i].length; j++) { if (bgMap[i][j] === '--') continue; idxX = str36.indexOf(bgMap[i][j].slice(0, 1)); idxY = str36.indexOf(bgMap[i][j].slice(1, 2)); //trace('bgMap[' + i + '][' + j + ']=' + bgMap[i][j] + ', idxY=' + idxY + ', idxX=', idxX ); bgBoard.copyPixels(mapSrc, new Rectangle(idxX*sz,idxY*sz,sz,sz), new Point(j*sz,i*sz), null, null, true); } } //bitmapDataを表示するためには、Bitmapが必要 var bg:Bitmap = new Bitmap(bgBoard); bg.x = -12; bg.y = -8; addChild(bg); //背景 上層 (樹木など) bgMap = []; bgMap.push('-- -- -- -- -- -- -- -- -- -- -- of pf -- --'.split(' ')); bgMap.push('-- -- -- -- -- -- -- -- -- -- -- -- -- p4 --'.split(' ')); bgMap.push('-- -- -- t7 -- -- oe pe -- -- -- -- -- -- --'.split(' ')); bgMap.push('-- -- -- -- -- -- of pf -- -- -- -- -- -- --'.split(' ')); bgMap.push('-- -- -- -- -- -- -- -- -- -- -- -- -- -- --'.split(' ')); bgMap.push('-- -- -- -- oe pe j9 -- -- -- -- -- -- -- --'.split(' ')); bgMap.push('je ke -- -- of pf -- -- -- -- -- -- -- -- --'.split(' ')); bgMap.push('jf kf -- -- -- -- -- -- -- -- -- -- -- -- --'.split(' ')); bgMap.push('-- -- -- -- -- -- -- -- -- -- -- -- -- -- --'.split(' ')); bgMap.push('-- -- -- -- -- -- -- -- -- -- -- -- -- -- --'.split(' ')); bgMap.push('-- -- -- -- -- -- -- qb rb sb tb -- -- -- --'.split(' ')); bgMap.push('-- -- -- -- -- -- -- qc rc sc tc -- -- -- --'.split(' ')); bgMap.push('-- -- -- -- -- -- -- qd rd sd td -- -- -- ia'.split(' ')); bgMap.push('-- -- -- -- -- -- -- qe re se te -- -- -- --'.split(' ')); bgMap.push('-- -- -- -- -- -- -- qf rf sf tf -- -- -- --'.split(' ')); var bgUpperBoard:BitmapData = new BitmapData(480, 480, true, 0x00000000); for (i = 0; i < bgMap.length; i++) { for (j = 0; j < bgMap[i].length; j++) { if (bgMap[i][j] === '--') continue; idxX = str36.indexOf(bgMap[i][j].slice(0, 1)); idxY = str36.indexOf(bgMap[i][j].slice(1, 2)); bgUpperBoard.copyPixels(mapSrc, new Rectangle(idxX*sz,idxY*sz,sz,sz), new Point(j*sz,i*sz), null, null, true); } } //キャラ var charaSrcOriginal:Bitmap = new CharaSrcFile(); var charaSrc:BitmapData = new BitmapData(charaSrcOriginal.width * 2, charaSrcOriginal.height * 2, true, 0x00000000); mtx = new Matrix(); mtx.scale(2, 2); charaSrc.draw(charaSrcOriginal, mtx); var szX:int = 24 * 2; //tip size X var szY:int = 32 * 2; //キャラクターチップを分割 charaTip = []; for (y = 0; y <= charaSrc.height/szY; y++) { charaTip[y] = []; for (x = 0; x < charaSrc.width/szX; x++) { charaTip[y][x] = new BitmapData(szX, szY, true, 0x00000000); charaTip[y][x].copyPixels(charaSrc, new Rectangle(x * szX, y * szY, szX, szY), new Point(0, 0)); } } //キャラを表示 chara = new Bitmap(charaTip[2][3]); chara.x = 8 * sz - 12; chara.y = 7 * sz - 8; addChild(chara); stage.addEventListener(KeyboardEvent.KEY_DOWN, keyDownHandler); //背景 上層 var bgUpper:Bitmap = new Bitmap(bgUpperBoard); bgUpper.x = -12; bgUpper.y = -8; addChild(bgUpper); } private function keyDownHandler(e:KeyboardEvent):void { var pattern:Array = [0, 1, 2, 1]; //0→1→2→0→1だとヘン。0→1→2→1→0→1にする。 var moveUnit:int = stage.frameRate / 8; //移動単位 px var charaKind:int = 3; count++; if (count % (stage.frameRate/6) === 0){ index = (index + 1) % pattern.length; } if (e.keyCode === Keyboard.UP) { chara.bitmapData = charaTip[0][pattern[index]+charaKind]; chara.y -= moveUnit; } else if (e.keyCode === Keyboard.RIGHT) { chara.bitmapData = charaTip[1][pattern[index]+charaKind]; chara.x += moveUnit; } else if (e.keyCode === Keyboard.DOWN) { chara.bitmapData = charaTip[2][pattern[index]+charaKind]; chara.y += moveUnit; } else if (e.keyCode === Keyboard.LEFT) { chara.bitmapData = charaTip[3][pattern[index]+charaKind]; chara.x -= moveUnit; } } } }
マップチップを数字見て配置するのがたいへんだ。配置ツール作らないと作業効率だ。
あと マップチップ配置に、http://www.tekepon.net/fsm/modules/refmap/index.php?mode=map&sort=released&cid=4の「プレビュー」で見ることのできる画像を参考にさせていただいたんだが、「プレビューにあるチップが、ダウンロード素材に含まれてないことがあるような‥‥。RPGツクール2000なら使えるのかな?