ヘキサ日記 Blog

 

        

みなさま、始めまして。

4月に入社した、新人プログラマーのふじひーです。

 

プログラムのネタを何かやろうかと思いましたが、にわかプログラマーの僕が適当な事かいてボコボコにされる未来が見えたので、全く需要の無い事にします。

需要の無い事それはずばり、ラーメン作りをして見ようと思います。

 

 

 

では早速、作り始めていきます。

ラーメンと言ったらまずは麺ですよね。

という事でパスタを用意します。

pasuta

茹でないと食べられないので茹でます。

 

yudeta

大体バリカタくらいでしょうか。

え?本物のラーメンじゃないって?

勿論料理スキルは0なので全く需要の無いシェーダーで作っていきます。

 麺だけじゃまだラーメンには見えないですよね。と言うか食べ物にも見えない。

きっとラーメンどんぶりがないのでラーメンに見えないのでしょう。

 

donburi

ラーメンどんぶりも用意しました。

立体には見えないですが仕方が無いです。

モデリングツールもイラストツールを持っていないのでシェーダーで頑張ります。

 

さっき茹でたパスタを盛り付ける為に余分な部分を捨てて盛り付けましょう。

mencut

 

 

合体!

inmen

カップラーメンを上から覗いたように見えるような見えないような。

あとはスープが無いとただの素麺になってしまうのでスープを作ります。

 

soup

作りすぎました。

素材は揃ったので全部盛り付けて

 

ramen

完成しました。

か、完成しました。

う~ん。

メシテロ感0ですね。

具も何も無いし、スペキュラーも無いので立体感もありません。

でも安心してください、好きな具材を乗せられるようにソースコードを公開します。

お好きにチャーシューでも乗せてください。

それでは。

 

 以下ラーメンソースコード。

// ノイズの生成は
// https://github.com/ashima/webgl-noise
// ここからお借りしました。
float snoise(float3 v);

// アルファブレンド
float3 blend(float3 col1, float3 col2, float a)
{
	return col1 * a + col2 * (1 - a);
}

// 麺を1玉作る
float4 tama(float2 uv, float menCount)
{
	float menVol = 0.7;
	float2 uvVec = uv * 2 - 1;
	float uvLen = length(uvVec);
	float vol = -uvLen + menVol;
	vol = max(vol, 0);

	// 茹でる前のパスタを並べる
	float h = sin(fmod(uv.x * PI * menCount, PI * 2)) * vol;
	float a = step(0.1, h);
	h = max(h, 0);
	h = lerp(0.5, 1, h);

	return float4(h.xxx, a);
}

// 麺を茹でる
float2 boil(float2 uv, float3 seed)
{
	// 麺が柔らかくなる
	float d = snoise(seed);
	uv.x += d * 0.01;

	return uv;
}

// 麺を作る
float4 makeMen(float2 uv)
{
	float4 col;
	float4 yudeMen;

	// 麺を1玉茹でる
	col = tama(boil(uv, float3(uv * 20, 2)), 60);

	// 麺を1玉茹でる
	yudeMen = tama(boil(uv.yx, float3(uv * 10, 0.5)), 60);
	col.rgb = blend(col.rgb, yudeMen.rgb, col.a);
	col.a = min(yudeMen.a + col.a, 1);

	// 麺を1玉茹でる
	yudeMen = tama(boil(uv, float3(uv * 15, 1)), 40);
	col.rgb = blend(col.rgb, yudeMen.rgb, col.a);
	col.a = min(yudeMen.a + col.a, 1);

	col = min(col, float4(1, 1, 1, 1));
	return col;
}

// どんぶりを作る
float makeDonburi(float2 uv, out float inside, out float depth)
{
	float donburiSize = 0.8;
	float thickness = 0.05;
	float2 uvVec = uv * 2 - 1;
	float uvLen = length(uvVec);

	// どんぶりの厚み
	inside = step(uvLen, donburiSize - thickness);
	float outside = step(uvLen, donburiSize);

	// どんぶりの内側
	float inner = uvLen / (1 - thickness);
	depth = 1-inner;
	inner = pow(inner, 5.0);
	inner = lerp(0.2, 0.9, inner);
	inner *= inside;

	// どんぶり合体
	return outside * (1 - inside) + inner;
}

// スープを作る
float4 makeSoup(float2 uv, float depth)
{
	// 油っぽいのを浮かせる
	float h = clamp(snoise(float3(uv * 8, 20)), 0, 1);
	h = pow(h, 0.4);
	float col = lerp(0.7, 1.0, h);
	
	// スープを注ぐ
	depth = smoothstep(0.25, 1, depth);
	depth = pow(depth, 0.2);
	
	return float4(col.xxx, depth);
}

// ラーメンを作る
float4 makeRamen(float2 uv)
{
	float3 menColor = float3(1, 1, 0.38);
	float3 soupColor = float3(0.93, 0.64, 0.1);
	float3 col = float3(1.0, 1.0, 1.0);

	// どんぶりを作る
	float inside;
	float depth;
	float donburi = makeDonburi(uv, inside, depth);

	// 麺を作る
	float4 menCol;
	menCol = makeMen(uv);
	// (どんぶりに入らない部分は捨てる)
	menCol *= inside;

	// スープを作る
	float4 soupCol = makeSoup(uv, depth);
	soupCol *= inside;

	// 盛り付け
	col = donburi.xxx;
	col = blend(menCol.rgb * menColor, col, menCol.a);
	col = blend(soupCol.rgb * soupColor, col, soupCol.a);

	return float4(col, 1);
}

        

ラーメン屋でスタンプカード出すのを忘れがちな某(なにがし)です

 

過去にシュンスケさんが32bitカラー画像を16bitカラーに減色する方法を紹介してくれました。

 

32とか16とかその5
↑今回関係するのはこのへん

 

「組織的ディザ法」「誤差分散法」を試して、誤差分散法が結構イイカンジという話でした。

 

私のチームで、シュンスケさんの実装を借用してみたところ、
それなりにイイカンジではありますが、元画像からの劣化がやっぱり気にはなりました。

 

元画像
20170915_original

 

減色後
20170915_rgba4444

 

※画像は「魔法パスワード1111」で使われている背景画像です。
 減色による画質劣化がわかりやすいものを選びました。

 

画像全体にノイズのようなドットが目立ちます

 

そもそもこの元画像、見ての通り不透明な背景画像です。

 

つまり、アルファチャンネルを使っていないので、
RGBA各チャンネルを4bitずつ表現しているRGBA4444形式では、
アルファチャンネルの4bitがすべて無駄になっているということになります。

 

この無駄な4bitをうまく利用してやれば、さらにイイカンジにできそうです

 

16bitカラーと言っても、RGBA4444だけでなく、RGB565という形式もあります。
名前の通り、RGBがそれぞれ565bitで構成されて16bitとなります。

 

アルファチャンネルがないので、不透明画像に適しています。
緑(G)が1bitだけ他のチャンネルより大きいのが面白いですね。
1bit増えると表現可能な色数が倍になりますので、大きな差になります。

 

それでは、シュンスケさんの実装をRGBA4444からRGB565に対応させてみます

 

特定の桁数から、任意の桁数に収まるよう端数を切る処理が必要なので、
例えば、10桁から6桁に落とすなら、上位6桁を持ってきて、下位4桁を捨てる考えでいいでしょう。

 

32bitフルカラーからの減色の場合、1チャンネルあたり8bit(1byte)なので、
Nビットに落とすなら、上位Nビットを取ってくればOKです。

 

// 1ピクセルあたりのバイト数
private const int PIXEL_BYTES = 4;

// 1バイトあたりのビット数
private const int BYTE_BITS = 8;

// 減色モード
private enum Mode
{
	RGBA4444,
	RGB565,
}

// チャネル
private enum Channel
{
	R,
	G,
	B,
	A,
}

private static uint quantizeChannel(uint value, Mode mode, Channel channel)
{
	var bitCount = BYTE_BITS;

	switch(mode) {
	case Mode.RGBA4444:
		bitCount = 4;
		break;
	case Mode.RGB565:
		if(channel == Channel.A) {
			bitCount = 0;
		}
		else if(channel == Channel.G) {
			bitCount = 6;
		}
		else {
			bitCount = 5;
		}
		break;
	default:
		break;
	}

	if(bitCount >= BYTE_BITS) return value;
	if(bitCount <= 0) return 0xFF;

	// 上位bitCountビット分を返す
	var shiftBits = BYTE_BITS - bitCount;
	return value & (~0u << shiftBits);
}

private static uint channelFilter(uint org, int x, int y, ref double[,] err, Mode mode, Channel channel)
{
	// 蓄積された誤差を加えて近似
	var tmp = (err[x, y] + org);
	if(tmp < 0) {
		tmp = 0;
	}
	if(tmp > 255) {
		tmp = 255;
	}
	var c = quantizeChannel((uint)Math.Round(tmp), mode, channel);

	// 誤差を分散して蓄積
	double e = tmp - c;
	for(int i = 0; i < DISPERSION_TBL.Length; ++i) {
		var p = DISPERSION_TBL[i];
		var xIdx = x + p.offsetX;
		var yIdx = y + p.offsetY;
		var xLen = err.GetLength(0);
		var yLen = err.GetLength(1);
		if(xIdx < 0 || xIdx >= xLen ||
		yIdx < 0 || yIdx >= yLen) continue;

		err[xIdx, yIdx] += (p.rate * e);
	}

	return c;
}

private static void filter(ref byte[] bmp, int w, int h, Mode mode)
{
	// 誤差格納用の領域を確保
	var errR = new double[w, h];
	var errG = new double[w, h];
	var errB = new double[w, h];
	var errA = new double[w, h];

	for(int i = 0; i < w; ++i) {
		for(int j = 0; j < h; ++j) {
			errR[i, j] = 0;
			errG[i, j] = 0;
			errB[i, j] = 0;
			errA[i, j] = 0;
		}
	}

	for(int y = 0; y < h; ++y) {
		for(int x = 0; x < w; ++x) {
			var index = (y * w + x) * PIXEL_BYTES;
			uint r = bmp[index + 0];
			uint g = bmp[index + 1];
			uint b = bmp[index + 2];
			uint a = bmp[index + 3];

			r = channelFilter(r, x, y, ref errR, mode, Channel.R);
			g = channelFilter(g, x, y, ref errG, mode, Channel.G);
			b = channelFilter(b, x, y, ref errB, mode, Channel.B);
			a = channelFilter(a, x, y, ref errA, mode, Channel.A);

			var color = Color.FromArgb((int)a, (int)r, (int)g, (int)b);
			bmp[index + 0] = color.R;
			bmp[index + 1] = color.G;
			bmp[index + 2] = color.B;
			bmp[index + 3] = color.A;
		}
	}
}

 

では、出力を比較してみましょう。

 

元画像
20170915_original

 

RGB565
20170915_rgb565

 

RGBA4444
20170915_rgba4444

 

全然違いますね。RGB565なら、ぱっと見では元画像と同じに見えます

 

ところで、Unity(5.4以前)でテクスチャフォーマットを16bit Colorで指定したとき、
元画像が不透明だと自動でRGB565に変換されます(※)が精度は良くないです・・・

 

ここで今回の手法を適用した画像を使うと、Unityの自動変換による劣化をかなり防げます

 

※Unity 5.5からテクスチャフォーマットの直接指定はプラットフォーム別にしかできなくなりました。
 旧バージョンからアップグレードした時には、metaに中途半端な設定が残ることもあるので注意しましょう。
 エディタ上で「RGB 16 bit」と表示されるものが「RGB565」に相当します。
(参考:Unity – Manual: Texture compression formats for platform-specific overrides

 

こういう減色ツールが手軽に使えるものとしてなかったりしますが、
原理は簡単なので作ってしまえば何かと融通がききそうです


        

みなさんこんにちは。グリフォンです

 

さて今回はシリーズでお届けしてきた「フォントのDistanceFieldTexture生成ツールをつくる」のその5。これが最後です。

 

大まかな処理の流れは以下

フォントを1文字ずつ読み出してピクセル情報を取得

ピクセル情報からDistance Field情報を生成

CharacterInfoを生成

アトラステクスチャに書き込む

アトラステクスチャとFontSettingsを出力保存

 

 

今回はを説明していきます。(ラスト

// アトラステクスチャの書き出し
File.WriteAllBytes("D:/FontDistanceFieldGenerator/Project/Assets/" + _OutputFontTextureName, _OutputFontTexture.EncodeToPNG());

// フォント設定ファイルの書き出し
{
	// 出力用にフォントクラスを生成
	var outputFont = new Font(_TextArea.font.name);
	outputFont.characterInfo	= _OutputFontModel._chars._charInfos.ToArray();

	// 既存フォント情報のシリアライズオブジェクトを生成
	var srcSerializedFont		= new SerializedObject(_TextArea.font);
	
	// 出力フォルト情報のシリアライズオブジェクトを生成
	var dstSerializedFont		= new SerializedObject(outputFont);
	
	// 必要情報をコピー
	dstSerializedFont.FindProperty("m_Tracking").floatValue		= srcSerializedFont.FindProperty("m_Tracking").floatValue;
	dstSerializedFont.FindProperty("m_LineSpacing").floatValue	= srcSerializedFont.FindProperty("m_LineSpacing").floatValue;
	dstSerializedFont.FindProperty("m_CharacterSpacing").intValue	= srcSerializedFont.FindProperty("m_CharacterSpacing").intValue;
	dstSerializedFont.FindProperty("m_CharacterPadding").intValue	= srcSerializedFont.FindProperty("m_CharacterPadding").intValue;
	dstSerializedFont.FindProperty("m_FontSize").floatValue		= srcSerializedFont.FindProperty("m_FontSize").floatValue;
	dstSerializedFont.FindProperty("m_Ascent").floatValue		= srcSerializedFont.FindProperty("m_Ascent").floatValue;
	dstSerializedFont.ApplyModifiedProperties();
	
	// 書き出し
	saveAsset(outputFont, "Assets/" + _TextArea.font.name + ".fontsettings");
}

 

今回は2つのファイルの書き出しを行います。

 

1つ目は、前回まで生成したアトラステクスチャを File.WriteAllBytes() で書き出します。(保存パスはご自由に)

 

2つ目は、 fontsettings を書き出します。これはで生成した文字情報のファイルですが、で生成した情報だけでは足りないので、元にしたフォントからコピーします。

 

fontsettings の書き出しは Unity のアセットとして保存する必要が有るので saveAsset() という関数で行っています。

private Object saveAsset(Object obj, string filePath)
{
	// アセットデータベースを検索して既存のファイルがあれば取得する
	var existAsset = AssetDatabase.LoadMainAssetAtPath(filePath);
	if( existAsset != null ) {
		// 新しくアセット生成
		EditorUtility.CopySerialized(obj, existAsset);
		AssetDatabase.SaveAssets();
		return existAsset;
	}
	else {
		// 既存のアセットに上書き
		AssetDatabase.CreateAsset(obj, filePath);
		AssetDatabase.Refresh();
		return obj;
	}
}

 

以上で、Distance Field Textureを使用したフォントを生成するツールの完成です

 

 実は、このツールは既にリリースされている以下のタイトルで使用しています。

どんな感じの見た目か気になる方は是非アプリをプレイして見てください。

 

20170913_Pouch

『 アイテム代は経費で落ちない~no item, no quest~』

 

20170913_Wand

『魔法パスワード1111』

 

長々とお付き合いいただき、ありがとうございます。

この内容が、みなさんのゲーム制作のお役に立てば幸いです


        

黒くてしゅわしゅわしてて甘くて美味しいあの飲み物

そう、コーラ

幸せを分け合うときには欠かせないですよね

そんなコーラを毎日2㍑飲む生活をはじめました

こんにちは、だっちです

 

先日プレゼントに貰ったコーラ24㍑があるので、

仕事をしながら飲んでいるといつの間にかペットボトルが空に…なんて日々を送っています。

健康のためには少し自制しないといけませんね

 

 

さて、前回に引き続きExcelで作成したデータから、

ゲーム内で扱いやすい形に変換するためのプログラムを実装していこうと思います。

 

前回は「型を生成するために必要な情報」を読み出すところまで出来たので、

今回は「クラスファイルの生成」をしていきたいと思います。

 

前回作成したプログラムを改良して、クラスファイルを生成していきます。

コードはこんな感じです。

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using NPOI.SS.UserModel;

namespace NPOITest
{
	class Program
	{
		// クラス情報
		private class ClassInfo
		{
			public string						name					{ get; }		//!< クラス名
			public string						desc					{ get; }		//!< クラス説明
			public List		memberVariableInfos		{ get; }		//!< メンバ変数情報

			// @brief	コンストラクタ
			// @param	[in]	name					クラス名
			// @param	[in]	desc					クラス説明
			// @param	[in]	memberVariableInfos		メンバ変数情報
			public ClassInfo(string name, string desc, List memberVariableInfos)
			{
				this.name					= name;
				this.desc					= desc;
				this.memberVariableInfos	= memberVariableInfos;
			}
		}

		// メンバ変数情報
		private class MemberVariableInfo
		{
			public string		name		{ get; }		//!< 変数名
			public string		type		{ get; }		//!< 型
			public string		desc		{ get; }		//!< 変数説明

			// @brief	コンストラクタ
			// @param	[in]	name	変数名
			// @param	[in]	type	型
			// @param	[in]	desc	変数説明
			public MemberVariableInfo(string name, string type, string desc)
			{
				this.name	= name;
				this.type	= type;
				this.desc	= desc;
			}
		}

		private const string	TYPEDEF_SHEET_NAME			= "typedef";		//!< 「型情報定義」シート名

		private const int		DATA_NAME_ROW				= 0;				//!< データ名行
		private const int		DATA_NAME_CELL				= 1;				//!< データ名列

		private const int		DATA_DESC_ROW				= 1;				//!< データ説明行
		private const int		DATA_DESC_CELL				= 1;				//!< データ説明列

		private const int		MEMBER_VARIABLE_START_ROW	= 10;				//!< メンバ変数開始行
		private const int		MEMBER_VARIABLE_NAME_CELL	= 1;				//!< メンバ変数名列
		private const int		MEMBER_VARIABLE_TYPE_CELL	= 2;				//!< メンバ変数型列
		private const int		MEMBER_VARIABLE_DESC_CELL	= 3;				//!< メンバ変数説明列
		private const int		MEMBER_VARIABLE_MAX			= 1024;				//!< メンバ変数最大許容数

		// @brief	エントリポイント
		public static void Main(string[] args)
		{
			// エクセルファイル入力受付
			Console.Write("Please input excel file path : ");
			var srcPath = Console.ReadLine();

			// 読み込み
			ClassInfo classInfo = null;
			readExcelFile(srcPath, out classInfo);

			// クラスファイル書き出し先入力受付
			Console.Write("Please input class file output directory : ");
			var outDirectory = Console.ReadLine();

			// 書き出し
			writeClassFile(outDirectory, classInfo);
		}

		// @brief	エクセルファイルの読み込み
		// @param	[in]	srcPath		読み込みファイル名
		// @param	[out]	classInfo	クラス情報
		// @return	なし
		private static void readExcelFile(string srcPath, out ClassInfo classInfo)
		{
			classInfo = null;

			// ファイル存在確認
			if( !File.Exists(srcPath) ) {
				Console.WriteLine("file not exists.");
				return;
			}

			var className			= string.Empty;
			var classDesc			= string.Empty;
			var memberVariableInfos	= new List();
			using( var fs = new FileStream(srcPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite) ) {
				var workbook = WorkbookFactory.Create(fs);

				// 中にあるシートを順番に取得
				foreach( ISheet sheet in workbook ) {
					if( sheet == null ) continue;

					// シート名表示
					Console.WriteLine("	Name : " + sheet.SheetName);

					// シートの名前が「型情報定義」シート名だったら、型情報を読み込み
					if( sheet.SheetName == TYPEDEF_SHEET_NAME ) {
						Console.WriteLine();

						Console.WriteLine("Data");
						// データ名を取得
						className = getCellValue(sheet, DATA_NAME_ROW, DATA_NAME_CELL);
						if( string.IsNullOrWhiteSpace(className) ) {
							Console.WriteLine("DataName acquisition failed...");
						}
						else {
							Console.WriteLine("	Name : " + className);
						}

						// データ説明を取得
						classDesc = getCellValue(sheet, DATA_DESC_ROW, DATA_DESC_CELL);
						if( string.IsNullOrWhiteSpace(classDesc) ) {
							Console.WriteLine("DataDesc acquisition failed...");
						}
						else {
							Console.WriteLine("	Desc : " + classDesc);
						}

						// メンバ変数を取得
						Console.WriteLine("MemberVariables");
						for( int i = 0, row = MEMBER_VARIABLE_START_ROW; i < MEMBER_VARIABLE_MAX; ++i, ++row ) {
							// メンバ変数名を取得
							var memberName = getCellValue(sheet, row, MEMBER_VARIABLE_NAME_CELL);
							if( string.IsNullOrWhiteSpace(memberName) ) {
								Console.WriteLine("MemberName acquisition failed...");
								// メンバ変数名が空欄だったら終了と判断
								Console.WriteLine("End Of Member Variables");
								break;
							}

							Console.WriteLine("	[" + i + "]");
							Console.WriteLine("		Name : " + memberName);

							// メンバ変数型を取得
							var memberType = getCellValue(sheet, row, MEMBER_VARIABLE_TYPE_CELL);
							if( string.IsNullOrWhiteSpace(memberType) ) {
								// メンバ変数名が空欄だったら終了と判断
								Console.WriteLine("MemberType acquisition failed...");
							}
							else {
								Console.WriteLine("		Type : " + memberType);
							}

							// メンバ変数説明を取得
							var memberDesc = getCellValue(sheet, row, MEMBER_VARIABLE_DESC_CELL);
							if( string.IsNullOrWhiteSpace(memberDesc) ) {
								// メンバ変数名が空欄だったら終了と判断
								Console.WriteLine("MemberDesc acquisition failed...");
							}
							else {
								Console.WriteLine("		Desc : " + memberDesc);
							}

							memberVariableInfos.Add(new MemberVariableInfo(memberName, memberType, memberDesc));
						}
					}
				}
			}

			classInfo = new ClassInfo(className, classDesc, memberVariableInfos);
		}

		// @brief	クラスファイルの書き出し
		// @param	[in]	outDirectory	書き出し先ディレクトリ
		// @param	[in]	classInfo		クラス情報
		// @return	なし
		private static void writeClassFile(string outDirectory, ClassInfo classInfo)
		{
			// 入力情報が不正なら終了
			if( string.IsNullOrWhiteSpace(outDirectory) ) {
				Console.WriteLine("outDirectory is invalid....");
				return;
			}
			if( classInfo == null ||
				string.IsNullOrWhiteSpace(classInfo.name) ||
				string.IsNullOrWhiteSpace(classInfo.desc) ||
				classInfo.memberVariableInfos == null ) {
				Console.WriteLine("classInfo is invalid...");
				return;
			}

			// 指定ディレクトリがなければ作成
			if( Directory.Exists(outDirectory) ) {
				Directory.CreateDirectory(outDirectory);
			}

			// ソースコード作成
			var sourceCode = new StringBuilder();

			// クラス開始
			sourceCode.AppendLine("// @class	" + classInfo.name);
			sourceCode.AppendLine("// @brief	" + classInfo.desc);
			sourceCode.AppendLine("public class " + classInfo.name);
			sourceCode.AppendLine("{");

			// プロパティ開始
			foreach( var memberVariableInfo in classInfo.memberVariableInfos ) {
				if( memberVariableInfo == null ||
					string.IsNullOrWhiteSpace(memberVariableInfo.name) ||
					string.IsNullOrWhiteSpace(memberVariableInfo.type) ||
					string.IsNullOrWhiteSpace(memberVariableInfo.desc) ) continue;

				sourceCode.AppendLine("	public " + memberVariableInfo.type + " " + memberVariableInfo.name + " { get; } //!< " + memberVariableInfo.desc);
			}
			sourceCode.AppendLine();
			// プロパティ終了

			// コンストラクタ開始
			sourceCode.AppendLine("	// @brief	コンストラクタ");
			// コンストラクタコメント
			foreach( var memberVariableInfo in classInfo.memberVariableInfos ) {
				if( memberVariableInfo == null ||
					string.IsNullOrWhiteSpace(memberVariableInfo.name) ||
					string.IsNullOrWhiteSpace(memberVariableInfo.type) ||
					string.IsNullOrWhiteSpace(memberVariableInfo.desc) ) continue;

				sourceCode.AppendLine("	// @param	[in]	" + memberVariableInfo.name + "	" + memberVariableInfo.desc);
			}
			// 引数
			var constructorArgs = new StringBuilder();
			foreach( var memberVariableInfo in classInfo.memberVariableInfos ) {
				if( memberVariableInfo == null ||
					string.IsNullOrWhiteSpace(memberVariableInfo.name) ||
					string.IsNullOrWhiteSpace(memberVariableInfo.type) ||
					string.IsNullOrWhiteSpace(memberVariableInfo.desc) ) continue;

				constructorArgs.Append(memberVariableInfo.type + " " + memberVariableInfo.name + ", ");
			}
			// 最後の不要な「, 」削除
			constructorArgs.Remove(constructorArgs.Length - 2, 2);
			sourceCode.AppendLine("	public " + classInfo.name + "(" + constructorArgs.ToString() + ")");
			sourceCode.AppendLine("	{");
			foreach( var memberVariableInfo in classInfo.memberVariableInfos ) {
				if( memberVariableInfo == null ||
					string.IsNullOrWhiteSpace(memberVariableInfo.name) ||
					string.IsNullOrWhiteSpace(memberVariableInfo.type) ||
					string.IsNullOrWhiteSpace(memberVariableInfo.desc) ) continue;

				sourceCode.AppendLine("		this." + memberVariableInfo.name + " = " + memberVariableInfo.name + ";");
			}
			sourceCode.AppendLine("	}");
			// コンストラクタ終了

			sourceCode.AppendLine("}");
			// クラス終了

			// 書き出し
			using( var sw = new StreamWriter(outDirectory + classInfo.name + ".cs", false, new UTF8Encoding(true)) ) {
				sw.Write(sourceCode.ToString());
			}
		}

		// @brief	セルの値を取得
		// @param	[in]	sheet		シート
		// @param	[in]	rowIndex	行インデックス
		// @param	[in]	cellIndex	列インデックス
		// @return	セルの値
		private static string getCellValue(ISheet sheet, int rowIndex, int cellIndex)
		{
			if( sheet == null ) return string.Empty;

			// 行を取得
			var row = sheet.GetRow(rowIndex);
			if( row == null ) return string.Empty;

			// 行からセルを取得
			var cell = row.GetCell(cellIndex);
			if( cell == null ) return string.Empty;

			// セルが取得できたら中身を文字列化
			return cell.ToString();
		}
	}
}

 

実際に生成されたコードはこんな感じです。

// @class	Weapon
// @brief	武器
public class Weapon
{
	public uint id { get; } //!< ID
	public string name { get; } //!< 名前
	public string desc { get; } //!< 説明
	public int attack { get; } //!< 攻撃力

	// @brief	コンストラクタ
	// @param	[in]	id	ID
	// @param	[in]	name	名前
	// @param	[in]	desc	説明
	// @param	[in]	attack	攻撃力
	public Weapon(uint id, string name, string desc, int attack)
	{
		this.id = id;
		this.name = name;
		this.desc = desc;
		this.attack = attack;
	}
}

 

これで、「クラスファイル」を生成するところまで完成しました

 

 

次回は、実際にデータファイルの書き出しをしていきたいと思います。

ではでは~


2017年8月22日

32とか16とかその7

        

こんにちは。

 

シュンスケです

 

前回でGAの処理が確認出来たので、いよいよ減色に取り入れて、

誤差拡散を超えてみようと思います

 

まずは、前回も記載した方針の通り、修正を行っていきます。

.使用する色を1チャンネル16色に制限する

.現在開始画像が完全にランダムな絵だったのを、誤差拡散等で

 既にキレイな状態を出発点にする

.評価式にPSNRを使用しているのを、SSIMに変更する

.今までの突然変異だと変化が大きすぎて細やかな改善に向かないので、

 ごく一部の変異に修正

.改善画像出力機能の作成

 

評価式SSIMは、PSNRよりも人間の認識に近い画像評価方法として出てきたもので、

画像の性質次第では事前にガウシアンフィルタによるノイズ除去を行うようです

が、減色の性質や今回のテスト画像の性質から無しで行ってみようと思います。

また、今回はチャンネル単位でSSIMを算出してその平均を画像の評価としています

が、横1ライン毎の各チャンネルで算出したものの平均にするなど、細かく分ける

のも有効そうです

 

では、実行結果。

まずはツールの様子

20170823_ga_window

 

そして、作成された画像

20170823_ga_org

20170823_ga_new

上が誤差拡散、下がそれを元に改善した画像

 

変わっ……った

 

確かに数値は上がっているのですが、元が良いのもあり正直わかりません

 

わかりやすいように、差分画像を作成しました。

(差分0を0xffとしてわかりやすくしています)

 

20170823_ga_diff

 

こう見ると、まばらにでも思ったより多くの箇所が改善されている結果、

評価値の向上に繋がっているようです

後は、どういった画像に使用するのか、使える時間はどのくらいかで

調整をしていけば、少なくとも SSIM基準による画質向上は見込めそうですが、

なかなか実感できる程を目指すのは難しいようです。

誤差拡散のお手軽さを考えると、この一線を超えるかは、 どこまでこだわるか

との相談になりそうですね

 

というわけで長きに渡って減色について続けてきたシリーズですが、

ここで一旦の区切りとします。

 

最後に今回のソースコードを貼っておきます。

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Windows.Forms;
using System.IO;
using System.Runtime.InteropServices;

namespace Pic32bitTo16bitGA
{
	class Program : Form
	{
		struct Gene
		{
			// 2点交叉
			public static void cross(ref Gene a, ref Gene b)
			{
				if( a.buf == null || b.buf == null ) return;

				var len = a.buf.Length;
				var r = new Random();
				var index1 = r.Next(0, len);
				var index2 = index1;
				if( index1 > len / 2 ) {
					index1 = r.Next(0, index2);
				}
				else {
					index2 = r.Next(index1 + 1, len);
				}
				for( int i = index1; i <= index2; ++i ) {
					var tmp = a.buf[i];
					a.buf[i] = b.buf[i];
					b.buf[i] = tmp;
				}
			}

			// SSIM算出
			private static double calcSSIM(byte[] x, byte[] y, int start, int end, int offset)
			{
				if( x == null || y == null || x.Length != y.Length || x.Length == 0 ) return 0;

				// 事前計算
				double c1 = (0.01 * 255) * (0.01 * 255);
				double c2 = (0.03 * 255) * (0.03 * 255);
				double avgX = 0, avgY = 0;	//!< 平均
				double sdX = 0, sdY = 0;	//!< 標準偏差
				double cov = 0;				//!< 共分散
				double len = 0;
				for( int i = start; i < end; i += offset, ++len ) {
					avgX += x[i];
					avgY += y[i];
				}
				avgX /= len;
				avgY /= len;
				len = 0;
				for( int i = start; i < end; i += offset, ++len ) {
					double dX = x[i] - avgX;
					double dY = y[i] - avgY;
					sdX += dX * dX;
					sdY += dY * dY;
					cov += dX * dY;
				}
				sdX = Math.Sqrt(sdX / len);
				sdY = Math.Sqrt(sdY / len);
				cov /= len;

				// SSIM算出
				return ((2 * avgX * avgY + c1) * (2 * cov + c2)) / ((avgX * avgX + avgY * avgY + c1) * (sdX * sdX + sdY * sdY + c2));
			}

			// コンストラクタ
			public Gene(int w, int h)
			{
				buf = new byte[w * h * PIXEL_BYTES];
				ssim = 0;
			}

			// 白で埋める
			public void fillWhite()
			{
				if( buf == null ) return;

				// 白色を埋める
				for( int i = 0; i < buf.Length; ++i ) {
					buf[i] = 0xff;
				}
			}

			// SSIM算出
			public void calcSSIM(byte[] org)
			{
				ssim = 0;
				if( org == null || buf == null || org.Length != buf.Length || org.Length == 0 ) return;

				var ssimR = calcSSIM(org, buf, 0, org.Length, 4);
				var ssimG = calcSSIM(org, buf, 1, org.Length, 4);
				var ssimB = calcSSIM(org, buf, 2, org.Length, 4);
				var ssimA = calcSSIM(org, buf, 3, org.Length, 4);

				// SSIM算出
				ssim = (ssimR + ssimG + ssimB + ssimA) / 4.0;
			}

			// 突然変異
			public void mutation()
			{
				if( buf == null ) return;

				var r = new Random();
				var index = r.Next(0, buf.Length);
				var c = r.Next(0, RGBA4444_COLORS.Length);
				buf[index] = RGBA4444_COLORS[c];
			}

			// 値を設定
			public void set(Gene g)
			{
				set(g.buf);
				ssim = g.ssim;
			}

			// バッファを元に値を設定
			public void set(byte[] src)
			{
				if( src == null ) return;

				if( buf == null || buf.Length != src.Length ) {
					buf = new byte[src.Length];
				}

				Array.Copy(src, buf, buf.Length);
			}

			public byte[]	buf;
			public double	ssim;
		}

		// 16bitカラーテーブル
		private static readonly byte[] RGBA4444_COLORS = new byte[] {
			0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff
		};
		private const int PIXEL_BYTES				= 4;								//!< 1ピクセルあたりのバイト数

		// エントリポイント
		static void Main(string[] args)
		{
			// 元画像パス入力受付
			Console.WriteLine("please input org picture path...");
			var orgPath = Console.ReadLine();

			// ファイル存在確認
			if( !File.Exists(orgPath) ) {
				Console.WriteLine("file not exists.");
				return;
			}

			// 画像読み込み
			byte[] orgBuf;
			int orgW;
			int orgH;
			if( readImg(out orgBuf, out orgW, out orgH, orgPath) ) {
				Console.WriteLine("org picture load failed.");
				return;
			}

			// 開始画像パス入力受付
			Console.WriteLine("please input start picture path...");
			var startPath = Console.ReadLine();

			// ファイル存在確認
			if( !File.Exists(startPath) ) {
				Console.WriteLine("file not exists.");
				return;
			}

			// 画像読み込み
			byte[] startBuf;
			int startW;
			int startH;
			if( readImg(out startBuf, out startW, out startH, startPath) ) {
				Console.WriteLine("start picture load failed.");
				return;
			}

			// サイズ確認
			if( orgW != startW ||
				orgH != startH ||
				orgBuf.Length != startBuf.Length ) {
				Console.WriteLine("org picture size and start picture size are different.");
				return;
			}

			// 出力先パスを作成
			var dstPath = orgPath.Substring(0, orgPath.Length - Path.GetFileName(orgPath).Length) + Path.GetFileNameWithoutExtension(orgPath) + "_new" + Path.GetExtension(orgPath);

			// フォーム開始
			Application.Run(new Program(orgBuf, startBuf, orgW, orgH, dstPath));
		}

		// 画像読み込み
		static bool readImg(out byte[] dst, out int width, out int height, string srcPath)
		{
			dst = null;
			width = 0;
			height = 0;
			using( var img = Image.FromFile(srcPath, true) as Bitmap ) {
				if( img == null ) {
					Console.WriteLine("file type is not support.");
					return true;
				}
				// メモリに保持
				var dat = img.LockBits(new Rectangle(Point.Empty, img.Size), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
				width = img.Width;
				height = img.Height;
				dst = new byte[width * height * PIXEL_BYTES];
				Marshal.Copy(dat.Scan0, dst, 0, dst.Length);

				// 解放
				img.UnlockBits(dat);
			}
			return false;
		}

		// コンストラクタ
		public Program(byte[] org, byte[] start, int w, int h, string dstPath)
		{
			if( org == null || start == null || dstPath == null ) return;

			_org = org;
			_start = start;
			_img = new Bitmap(w, h);
			_dstPath = dstPath;

			// 第1世代生成
			_startGen.set(_start);
			_startGen.calcSSIM(_org);
			for( int i = 0; i < _curGen.Length; ++i ) {
				_curGen[i] = new Gene(w, h);
				_curGen[i].set(_startGen);
			}

			// フォームのサイズ設定
			ClientSize = new Size(w, h * 2);

			// フォント作成
			_font = new Font(Font.Name, 10);
			_fontBrush = new SolidBrush(ForeColor);

			// 出力ボタン作成、登録
			_outBtn = new Button();
			_outBtn.Location = new Point(300, 0);
			_outBtn.Size = new Size(50, 20);
			_outBtn.Text = "出力";
			_outBtn.Click += new EventHandler(onClickOutBtn);
			Controls.Add(_outBtn);

			// 描画更新開始
			_sec = 0;
			_refreshTimer.Interval = 1000;
			_refreshTimer.Tick += new EventHandler(refresh);
			_refreshTimer.Start();

			// 処理更新開始
			_updateTimer.Interval = 1;
			_updateTimer.Tick += new EventHandler(update);
			_updateTimer.Start();
		}

		// 描画時処理
		protected override void OnPaint(PaintEventArgs e)
		{
			base.OnPaint(e);
			if( _img == null ) return;

			// 元画像描画
			if( _org != null ) {
				var newDat = _img.LockBits(new Rectangle(Point.Empty, _img.Size), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
				Marshal.Copy(_org, 0, newDat.Scan0, _org.Length);
				_img.UnlockBits(newDat);
				e.Graphics.DrawImage(_img, 0, 0);
			}

			// 生成画像描画
			if( _topGen.buf != null ) {
				var newDat = _img.LockBits(new Rectangle(Point.Empty, _img.Size), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
				Marshal.Copy(_topGen.buf, 0, newDat.Scan0, _topGen.buf.Length);
				_img.UnlockBits(newDat);
				e.Graphics.DrawImage(_img, 0, _img.Size.Height);
			}

			// フレーム数表示
			e.Graphics.DrawString("sec : " + _sec, _font, _fontBrush, 0, 0);

			// 世代数表示
			e.Graphics.DrawString("gen : " + _genCnt, _font, _fontBrush, 0, 10);

			// SSIM表示
			e.Graphics.DrawString("start ssim : " + _startGen.ssim, _font, _fontBrush, 100, 0);
			e.Graphics.DrawString("top   ssim : " + _topGen.ssim, _font, _fontBrush, 100, 10);
		}

		// 描画更新
		private void refresh(object sender, EventArgs e)
		{
			// 画面をリフレッシュ
			++_sec;
			Invalidate();
		}

		// 出力ボタン押下時処理
		private void onClickOutBtn(object sender, EventArgs e)
		{
			if( _img == null || _dstPath == null ) return;

			// トップの画像を書き出し
			// 出力画像作成
			var newImg = new Bitmap(_img.Width, _img.Height);
			var newDat = newImg.LockBits(new Rectangle(Point.Empty, newImg.Size), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
			Marshal.Copy(_topGen.buf, 0, newDat.Scan0, _topGen.buf.Length);
			newImg.UnlockBits(newDat);
			newDat = null;

			// 保存
			newImg.Save(_dstPath);
			newImg.Dispose();
			newImg = null;
		}

		// 処理更新
		private void update(object sender, EventArgs e)
		{
			// 次世代初期化
			for( int i = 0; i < _nextGen.Length; ++i ) {
				_nextGen[i].fillWhite();
			}

			// 評価値算出
			for( int i = 0; i < _curGen.Length; ++i ) {
				_curGen[i].calcSSIM(_org);
			}

			// 評価値でソート
			Array.Sort(_curGen, (a, b) => {
				if( a.ssim == b.ssim ) return 0;

				return (b.ssim > a.ssim) ? 1 : -1;
			});

			// 1位を保持
			_topGen.set(_curGen[0]);

			// 上位いくつかはそのまま残す
			var newIndex = 0;
			for( int i = 0; i < 2 && i < _nextGen.Length; ++newIndex, ++i ) {
				_nextGen[newIndex].set(_curGen[i]);
			}

			while( newIndex < _nextGen.Length ) {
				// 交叉対象1を選出(上位が選ばれやすいように)
				_cross1Buf.set(_curGen[0]);
				for( int i = 1; i < _curGen.Length; ++i ) {
					var rate = _selectRnd.Next(0, 100);
					if( rate < 10 ) {
						_cross1Buf.set(_curGen[i]);
						break;
					}
				}

				// 交叉対象2を選出(上位が選ばれやすいように)
				_cross2Buf.set(_curGen[1]);
				for( int i = 2; i < _curGen.Length; ++i ) {
					var rate = _selectRnd.Next(0, 100);
					if( rate < 10 ) {
						_cross2Buf.set(_curGen[i]);
						break;
					}
				}

				// 交叉
				Gene.cross(ref _cross1Buf, ref _cross2Buf);
				_nextGen[newIndex].set(_cross1Buf);
				++newIndex;
				if( _nextGen.Length <= newIndex ) break;

				_nextGen[newIndex].set(_cross2Buf);
				++newIndex;
				if( _nextGen.Length <= newIndex ) break;

				// 突然変異対象を選出
				var mutIndex = _selectRnd.Next(0, _curGen.Length);
				_nextGen[newIndex].set(_curGen[mutIndex]);
				_nextGen[newIndex].mutation();
				++newIndex;
				if( _nextGen.Length <= newIndex ) break;

				// そのままコピー
				var copyIndex = _selectRnd.Next(0, _curGen.Length);
				_nextGen[newIndex].set(_curGen[copyIndex]);
				++newIndex;
				if( _nextGen.Length <= newIndex ) break;
			}

			// 次世代を現世代に移す
			for( int i = 0; i < _nextGen.Length; ++i ) {
				_curGen[i].set(_nextGen[i]);
			}
			++_genCnt;
		}

		private Bitmap	_img			= null;
		private byte[]	_org			= null;
		private byte[]	_start			= null;
		private string	_dstPath		= null;

		private Timer	_refreshTimer	= new Timer();
		private Timer	_updateTimer	= new Timer();
		private Button	_outBtn			= null;
		private Font	_font			= null;
		private Brush	_fontBrush		= null;
		private int		_sec			= 0;
		private int		_genCnt			= 0;

		private Gene[]	_curGen			= new Gene[128];
		private Gene[]	_nextGen		= new Gene[128];
		private Random	_selectRnd		= new Random();
		private Gene	_cross1Buf		= new Gene();
		private Gene	_cross2Buf		= new Gene();
		private Gene	_topGen			= new Gene();
		private Gene	_startGen		= new Gene();
	}
}

 

では


        

お久しぶりです。
台湾で印象的だった食べ物が鳥の頭と鳥の脚、あとは臭豆腐なgood sunこと山口です。

 

さて前回のシリーズから半年ぶりですが、その実全く進歩していないという噂のC++ソース解析第三弾です。

 

今回は純粋にシンプルにVisual Studioのプロジェクトファイル内で使用されている型情報をザックリ拾い上げてみます。

 

このシリーズ、libclangの仕様を十分に理解していない為かそろそろ優しくなくなってきました。

 

思えばこのシリーズを始めてから長い月日がたったので大分C#も馴染みの言語になってきました。 
特筆してReflection(Attributes含む)とLINQとプロパティが素敵です。

 

早速ソースコードを掲載しながらやっていきましょう。

まずはVisual StudioのC++プロジェクトファイルであるvcxprojを読み込みます。
もしかしたら解析してくれるツールがあるのかもしれませんが、 見つけられなかったのでサクッと作ります

C#というか.Netさん素敵です。 目標はビルド対象のc++のピックアップとインクルードディレクトリの取得、プリプロセッサ定義の取得です。

 

ということでXmlTextReaderを利用してこんな感じで書きます

public class BuildInfo
{
	public String ProjPath { get; set; }
	public List SrcFiles { get; set; } = new List();
	public List IncludePathes { get; set; } = new List();
	public List PreprocessorDefs { get; set; } = new List();
}

public class AnalyzeVcxproj
{
	private Regex buildCondition;
	public BuildInfo Result{ get; private set; }
	public void Analyze(string vcxproj, string condition)
	{
		buildCondition = new Regex(condition, RegexOptions.IgnoreCase);
		Result = new BuildInfo();
		var currentElement = new Stack();
		currentElement.Push("");
		Result.ProjPath = Path.GetDirectoryName(vcxproj);
		// ビルド構成チェック
		bool isTargetCondition = false;
		bool bodyReadable = false;

		using (var reader = new XmlTextReader(vcxproj))
		{
			while (reader.Read())
			{
				switch (reader.NodeType)
				{
					case XmlNodeType.Element:
						{
							var lastElement = currentElement.Peek();
							if (!reader.IsEmptyElement)
							{
								currentElement.Push(reader.Name);
							}
							switch (lastElement)
							{
								case "ItemGroup":
									{
										if (currentElement.Peek() == "ClCompile")
										{
											while (reader.MoveToNextAttribute())
											{
												if (reader.Name == "Include")
												{
													Result.SrcFiles.Add(reader.Value);
													break;
												}
											}
										}
									}
									break;
								case "PropertyGroup":
									{
										if (currentElement.Peek() == "IncludePath")
										{
											bodyReadable = isTargetCondition;
										}
									}
									break;
								case "ClCompile":
									{
										if (currentElement.Peek() == "PreprocessorDefinitions")
										{
											bodyReadable = isTargetCondition;
										}
									}
									break;
								default:
									{
										if (currentElement.Peek() == "PropertyGroup")
										{
											if (!reader.IsEmptyElement)
											{
												// Conditionの簡易チェック
												while (reader.MoveToNextAttribute())
												{
													if (reader.Name == "Condition")
													{
														isTargetCondition = buildCondition.IsMatch(reader.Value);
														break;
													}
												}
											}
										}
										else if (currentElement.Peek() == "ItemDefinitionGroup")
										{
											if (!reader.IsEmptyElement)
											{
												// Conditionの簡易チェック
												while (reader.MoveToNextAttribute())
												{
													if (reader.Name == "Condition")
													{
														isTargetCondition = buildCondition.IsMatch(reader.Value);
														break;
													}
												}
											}
										}
									}
									break;
							}
						}
						break;
					case XmlNodeType.Text:
						if (bodyReadable)
						{
							switch (currentElement.Peek())
							{
								case "IncludePath":
									if (Result.IncludePathes.Count == 0)
									{
										Result.IncludePathes = reader.Value.Split(';').Where(x => x[0] != '$').ToList();
									}
									break;
								case "PreprocessorDefinitions":
									if (Result.PreprocessorDefs.Count == 0)
									{
										Result.PreprocessorDefs = reader.Value.Split(';').Where(x => x[0] != '%').ToList();
									}
									break;
							}
							bodyReadable = false;
						}
						break;
					case XmlNodeType.EndElement:
						currentElement.Pop();
						break;
					default:
						break;
				}
			}
		}
	}
}

サクッとvcxprojの中で必要な情報を抜き出しました。
必要な情報を限定するとそこまで複雑では無いですね。
(VS2017を利用したので他のバージョンはダメかもです)

 

問題は型情報の収集です。
一番の苦労ポイントは単純に型のtypedefやnamespaceを解決した名前を取得する事でした。
もしかしたらもっとしっかりした解析方法があるかもしれませんが 今回はgetCanonicalType()を使ってみたところいい感じに取得できました。

なかなか参考ページにたどり着くまで苦労しますね

	public class AnalyzedField
	{
		public String TypeName { get; set; }
		public String VariableName { get; set; }
		public int ArraySize { get; set; }
		public bool IsPtr { get; set; }
	}
	public class AnalyzedClass
	{
		public bool IsTemplate { get; set; }
		public String NSpace{ get; set; }
		public String Name { get; set; }
		public List Member { get; set; } = new List();
		public List Bases { get; set; } = new List();
	}
	public class AnalyzedEnum
	{
		public String NSpace { get; set; }
		public String Name { get; set; }
		public List Member { get; set; } = new List();
	}
	public class SubAnalyzer
	{
		private List nspace = new List();
		public List Classes { get; private set; } = new List();
		public List Enums { get; private set; } = new List();
		private AnalyzedEnum currentEnum;
		private AnalyzedClass currentClass;
		private CX_CXXAccessSpecifier currentAccess = CX_CXXAccessSpecifier.CX_CXXPrivate;
		public SubAnalyzer(CXCursor cursor)
		{
			clang.visitChildren(cursor, new CXCursorVisitor(ScanChild), new CXClientData());
		}
		private String ConnectedNameSpace
		{
			get { return nspace.Count == 0 ? "" : (nspace.Aggregate((x,y)=>x+"::"+y)); }
		}
		private CXChildVisitResult ScanChild(CXCursor cursor, CXCursor parent, IntPtr client_data)
		{
			switch (cursor.kind)
			{
				case CXCursorKind.CXCursor_Namespace:
					pushNamespace(cursor);
					clang.visitChildren(cursor, new CXCursorVisitor(ScanChild), new CXClientData());
					popNamespace();
					break;
				case CXCursorKind.CXCursor_StructDecl:
					if (currentClass == null)
					{
						var prevClass = currentClass;
						currentClass = new AnalyzedClass()
						{
							IsTemplate = false,
							Name = clang.getCursorSpelling(cursor).ToString(),
							NSpace = ConnectedNameSpace,
						};
						clang.visitChildren(cursor, new CXCursorVisitor(ScanChild), new CXClientData());
						Classes.Add(currentClass);
						currentClass = prevClass;
					}
					break;
				case CXCursorKind.CXCursor_ClassDecl:
					// クラスのネストは未対応で...
					if(currentClass == null){
						var prevClass = currentClass;
						currentClass = new AnalyzedClass()
						{
							IsTemplate = false,
							Name = clang.getCursorSpelling(cursor).ToString(),
							NSpace = ConnectedNameSpace,
						};
						clang.visitChildren(cursor, new CXCursorVisitor(ScanChild), new CXClientData());
						Classes.Add(currentClass);
						currentClass = prevClass;
					}
					break;
				case CXCursorKind.CXCursor_CXXBaseSpecifier:
					{
						// 基底クラス情報
						var typ = clang.getCursorType(cursor);
						// typedefとかの解除
						while (typ.kind == CXTypeKind.CXType_Typedef)
						{
							typ = clang.getCanonicalType(typ);
						}
						currentClass.Bases.Add(clang.getTypeSpelling(typ).ToString());
					}
					break;
				case CXCursorKind.CXCursor_EnumDecl:
					{
						// enumを作成してリストに追加する
						currentEnum = new AnalyzedEnum()
						{
							Name = clang.getCursorSpelling(cursor).ToString(),
							NSpace = ConnectedNameSpace,
						};
						clang.visitChildren(cursor, new CXCursorVisitor(ScanEnum), new CXClientData());
						Enums.Add(currentEnum);
						currentEnum = null;
					}
					break;
				case CXCursorKind.CXCursor_CXXAccessSpecifier:
					{
						currentAccess = clang.getCXXAccessSpecifier(cursor);
					}
					break;
				case CXCursorKind.CXCursor_FieldDecl:
					// クラスのメンバのみをスキャンする
					if(currentClass != null)
					{
						var type = clang.getCursorType(cursor);
						var arrCount = clang.getArraySize(type);
						var field = new AnalyzedField();
						field.ArraySize = (int)arrCount;
						CXType vtype;
						if (arrCount <= 0)
						{
							vtype = type;
						}
						else
						{
							vtype = clang.getArrayElementType(type);
						}
						while (vtype.kind == CXTypeKind.CXType_Typedef)
						{
							vtype = clang.getCanonicalType(vtype);
						}
						// ちょっと良く分からない
						if (vtype.kind == CXTypeKind.CXType_Unexposed)
						{
							break;
						}
						if (vtype.kind == CXTypeKind.CXType_Pointer)
						{
							field.IsPtr = true;
							vtype = clang.getPointeeType(vtype);
							while (vtype.kind == CXTypeKind.CXType_Typedef)
							{
								vtype = clang.getCanonicalType(vtype);
							}
						}
						else
						{
							field.IsPtr = false;
						}
						field.TypeName = clang.getTypeSpelling(vtype).ToString();
						field.VariableName = clang.getCursorSpelling(cursor).ToString();
						currentClass.Member.Add(field);
					}
					break;
				case CXCursorKind.CXCursor_ClassTemplate:
					{
						// クラスのネストは未対応で...
						if (currentClass == null)
						{
							var prevClass = currentClass;
							currentClass = new AnalyzedClass()
							{
								IsTemplate = true,
								Name = clang.getCursorSpelling(cursor).ToString(),
								NSpace = ConnectedNameSpace,
							};
							clang.visitChildren(cursor, new CXCursorVisitor(ScanChild), new CXClientData());
							Classes.Add(currentClass);
							currentClass = prevClass;
						}
					}
					break;
				case CXCursorKind.CXCursor_CXXMethod:
					break;
				default:
					break;
			}
			return CXChildVisitResult.CXChildVisit_Continue;
		}
		private CXChildVisitResult ScanEnum(CXCursor cursor, CXCursor parent, IntPtr client_data)
		{
			var itype = clang.getEnumDeclIntegerType(cursor);
			if (itype.kind == CXTypeKind.CXType_UInt)
			{
				currentEnum.Member.Add($"{clang.getCursorSpelling(cursor)},{clang.getEnumConstantDeclUnsignedValue(cursor)}");
			}
			else
			{
				currentEnum.Member.Add($"{clang.getCursorSpelling(cursor)},{clang.getEnumConstantDeclValue(cursor)}");
			}
			return CXChildVisitResult.CXChildVisit_Continue;
		}

		private void pushNamespace(CXCursor cursor)
		{
			var cxName = clang.getCursorSpelling(cursor);
			nspace.Add(cxName.ToString());
		}
		private void popNamespace()
		{
			nspace.RemoveAt(nspace.Count -1);
		}
	}
	public class CppAnalyzer
	{
		public void Analyze(BuildInfo info)
		{
			// インクルードパスオプション
			List cmdOptions = new List();
			cmdOptions.Add("-std=c++11");
			cmdOptions.AddRange(info.IncludePathes.Select(x=>"-I" + (Path.IsPathRooted(x) ? x : Path.Combine(info.ProjPath, x))).ToList());
			cmdOptions.AddRange(info.PreprocessorDefs.Select(x=>"-D" + x).ToList());

			foreach (var src in info.SrcFiles) {
				// ハッシュと依存関係から更新チェックする
				var dummy = new CXUnsavedFile();
				var index = clang.createIndex(0, 0);
				var trans = new CXTranslationUnit();
				// 第二回と違ってバッチあてるの面倒だったので
				var err = clang.parseTranslationUnit2(index, Path.Combine(info.ProjPath, src), cmdOptions.ToArray(), cmdOptions.Count, out dummy, 0, 0, out trans);
				if (err != CXErrorCode.CXError_Success)
				{
					// TODO:エラー出力
					continue;
				}
				// 解析する
				var cursor = clang.getTranslationUnitCursor(trans);
				var kind = clang.getCursorKind(cursor);
				var sub = new SubAnalyzer(cursor);
				// TODO異なる情報のみを収集
			}
		}
	}

と比較的短いコードでVisual Studioのプロジェクト内の型情報がある程度の粒度で集められました。
あとはこの情報を利用して外部からデータを変更するコードを書いたり、 シリアライズ、デシリアライズする仕組みを作ってレベルエディタ等に活用したりですね
今回未完成で申し訳ないですが翻訳単位1個だけピックアップしていて、 複数の翻訳単位でのマージなどは行っていません
同一クラス名を省く形でリストアップすると良いかもしれません。

 

次回まで気力が続くときっとシリアライズとかその辺のコードが書かれて本連載も終了となるハズです。

 

ではまた


        

みなさんこんにちは。

週末にボルダリングに行ったおかげで筋肉痛中のグリフォンです。

 

さて今回も前回の続き「フォントのDistanceFieldTexture生成ツールをつくる」のその4です。

 

大まかな処理の流れは以下

フォントを1文字ずつ読み出してピクセル情報を取得

ピクセル情報からDistance Field情報を生成

CharacterInfoを生成

アトラステクスチャに書き込む

アトラステクスチャとFontSettingsを出力保存

 

 

今回はを説明していきます。(後半戦です)

private bool WriteOutputFontTexture(string writeChar)
{
    var srcPixels = _BufferTexture.GetPixels32();
    var srcWidth  = _BufferTexture.width;
    var srcHeight = _BufferTexture.height;
    var dstPixels = _OutputFontTexture.GetPixels32();
    var dstWidth  = _OutputFontTexture.width;
    var dstHeight = _OutputFontTexture.height;

    while( _OutputPageNo < 4 ) {
        if( dstHeight > _OutputOffsetY + srcHeight ) {
            if( dstWidth > _OutputOffsetX + srcWidth ) {
                // 現在の行に収まる
                {
                    // ピクセルコピー
                    for( int y=0; y<srcHeight; ++y ) {
                        for( int x=0; x<srcWidth; ++x ) {
                            var srcPos = (y * srcWidth) + x;
                            var dstPos = ((_OutputOffsetY + y) * dstWidth) + (_OutputOffsetX + x);
                            switch( _OutputPageNo ) {
                                case 0: dstPixels[dstPos].r = srcPixels[srcPos].a; break;
                                case 1: dstPixels[dstPos].g = srcPixels[srcPos].a; break;
                                case 2: dstPixels[dstPos].b = srcPixels[srcPos].a; break;
                                case 3: dstPixels[dstPos].a = srcPixels[srcPos].a; break;
                            }
                        }
                    }

                    // テクスチャに反映
                    _OutputFontTexture.SetPixels32(dstPixels);
                    _OutputFontTexture.Apply();


                    /** ここに前回の「文字情報を成形」を挟みます **/
                    
                    
                    // 書き込み情報を更新
                    _OutputOffsetX     = _OutputOffsetX + srcWidth;
                    _OutputOffsetNextY = (int)Mathf.Max(_OutputOffsetNextY, _OutputOffsetY + srcHeight); // ←高さが高いオフセットを保存
                }
                return true; // 書き込み成功
            }
            else {
                // 次の行へ
                _OutputOffsetX     = 0;
                _OutputOffsetY     = _OutputOffsetNextY;
                _OutputOffsetNextY = 0;
            }
        }
        else {
            // 次のページへ
            _OutputPageNo++;
            _OutputOffsetX     = 0;
            _OutputOffsetY     = 0;
            _OutputOffsetNextY = 0;
        }

    return false; // 書き込み失敗
}

 

今回のアトラステクスチャはRGBAチャンネルに別々の文字郡を格納していきます。

 

_OutputPageNo が現在の格納先チャンネルを表しています。(0=R, 1=G, 2=B, 3=A)

 

_OutputOffsetX ・ _OutputOffsetY が書き込むピクセルの基準座標になります。

 

そして今回のポイントは、_OutputOffsetNextY です。

_OutputOffsetY が有るにも関わらず、なぜ別に必要なのか

 

それは、文字ごとに高さが違うからです。

今回のアトラステクスチャには、まずX方向に1行ずつ埋めていき、次にY方向に1行ずらしてまたX方向に埋めていきます。

 

そのため、_OutputOffsetY のみだと1行の最後の文字の高さが低い場合、次の行の文字が重なってしまいます。

20170516_ng

 

そこで、1行のうちで最も高い高さを _OutputOffsetNextY に保持して次の行のオフセットとして使います。

20170516_ok

これで文字が重なることはなくなります。

 

今回はここまで。次回はいよいよラストですお楽しみに


        

GW中にハイラルを救い、無事に勇者の仲間入りを果たすことができました

こんにちは、だっちです

(ハイラルから外に出るのは久しぶりなので現実世界のリハビリ中なのは内緒です)

 

 

さて、前回に引き続き、Excelで作成したデータから、

ゲーム内で扱いやすい形に変換するためのプログラムを実装していこうと思います。

 

今回は「型を生成するための情報」を定義していきたいと思います。

各種データを持つクラスを作成するためには、最低でも以下の情報が必要です。

・クラス情報

 ・名前

 ・説明

・メンバ変数情報

 ・名前

 ・型

 ・説明

 

「武器」のデータを例として、

上記の情報を実際にExcelに入力して「クラス(型)定義」として扱うための準備を行います。

Excelに入力するとこんな感じになります。

NPOITestExcelDataTypedefWeapon

 

 

前回作成したプログラムを改良してこの情報を読み込んでみます。
コードはこんな感じです。

using System;
using System.IO;
using NPOI.SS.UserModel;

namespace NPOITest
{
	class Program
	{
		private const string	TYPEDEF_SHEET_NAME		= "typedef";		//!< 「型情報定義」シート名

		private const int	DATA_NAME_ROW			= 0;			//!< データ名行
		private const int	DATA_NAME_CELL			= 1;			//!< データ名列

		private const int	DATA_DESC_ROW			= 1;			//!< データ説明行
		private const int	DATA_DESC_CELL			= 1;			//!< データ説明列

		private const int	MEMBER_VARIABLE_START_ROW	= 10;			//!< メンバ変数開始行
		private const int	MEMBER_VARIABLE_NAME_CELL	= 1;			//!< メンバ変数名列
		private const int	MEMBER_VARIABLE_TYPE_CELL	= 2;			//!< メンバ変数型列
		private const int	MEMBER_VARIABLE_DESC_CELL	= 3;			//!< メンバ変数説明列
		private const int	MEMBER_VARIABLE_MAX		= 1024;			//!< メンバ変数最大許容数

		// エントリポイント
		public static void Main(string[] args)
		{
			// 入力受付
			Console.Write("Please input excel file path : ");
			var srcPath = Console.ReadLine();

			// ファイル存在確認
			if( !File.Exists(srcPath) ) {
				Console.WriteLine("file not exists.");
				return;
			}

			// 読み込み
			using( var fs = new FileStream(srcPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite) ) {
				var workbook = WorkbookFactory.Create(fs);

				// 中にあるシートを順番に取得
				foreach( ISheet sheet in workbook ) {
					if( sheet == null ) continue;

					// シート名表示
					Console.WriteLine("	Name : " + sheet.SheetName);

					// シートの名前が「型情報定義」シート名だったら、型情報を読み込み
					if( sheet.SheetName == TYPEDEF_SHEET_NAME ) {
						Console.WriteLine();

						Console.WriteLine("Data");
						// データ名を取得
						var dataName = getCellValue(sheet, DATA_NAME_ROW, DATA_NAME_CELL);
						if( string.IsNullOrWhiteSpace(dataName) ) {
							Console.WriteLine("DataName acquisition failed...");
						}
						else {
							Console.WriteLine("	Name : " + dataName);
						}

						// データ説明を取得
						var dataDesc = getCellValue(sheet, DATA_DESC_ROW, DATA_DESC_CELL);
						if( string.IsNullOrWhiteSpace(dataDesc) ) {
							Console.WriteLine("DataDesc acquisition failed...");
						}
						else {
							Console.WriteLine("	Desc : " + dataDesc);
						}

						// メンバ変数を取得
						Console.WriteLine("MemberVariables");
						for( int i = 0, row = MEMBER_VARIABLE_START_ROW; i < MEMBER_VARIABLE_MAX; ++i, ++row ) {
							// メンバ変数名を取得
							var memberName = getCellValue(sheet, row, MEMBER_VARIABLE_NAME_CELL);
							if( string.IsNullOrWhiteSpace(memberName) ) {
								Console.WriteLine("MemberName acquisition failed...");
								// メンバ変数名が空欄だったら終了と判断
								Console.WriteLine("End Of Member Variables");
								break;
							}

							Console.WriteLine("	[" + i + "]");
							Console.WriteLine("		Name : " + memberName);

							// メンバ変数型を取得
							var memberType = getCellValue(sheet, row, MEMBER_VARIABLE_TYPE_CELL);
							if( string.IsNullOrWhiteSpace(memberType) ) {
								// メンバ変数名が空欄だったら終了と判断
								Console.WriteLine("MemberType acquisition failed...");
							}
							else {
								Console.WriteLine("		Type : " + memberType);
							}

							// メンバ変数説明を取得
							var memberDesc = getCellValue(sheet, row, MEMBER_VARIABLE_DESC_CELL);
							if( string.IsNullOrWhiteSpace(memberDesc) ) {
								// メンバ変数名が空欄だったら終了と判断
								Console.WriteLine("MemberDesc acquisition failed...");
							}
							else {
								Console.WriteLine("		Desc : " + memberDesc);
							}
						}
					}
				}
			}
		}

		// セルの値を取得
		private static string getCellValue(ISheet sheet, int rowIndex, int cellIndex)
		{
			if( sheet == null ) return string.Empty;

			// 行を取得
			var row = sheet.GetRow(rowIndex);
			if( row == null ) return string.Empty;

			// 行からセルを取得
			var cell = row.GetCell(cellIndex);
			if( cell == null ) return string.Empty;

			// セルが取得できたら中身を文字列化
			return cell.ToString();
		}
	}
}

 

 

実行結果

NPOITestExcelDataResultReadWeapon

「型を生成するための情報」をExcelから取得できていますね

 

 

次回は、今回取得した情報を元に「クラスファイル」を書き出していきたいと思います。

ではでは~


2017年4月11日

32とか16とかその6改

        

こんにちは。

 

シュンスケです

 

前回、イマイチうまく結果が出なかったGAですが、薄っすらと変化はありました

ただ、変化が上の方にしか無いので怪しいなという事で追っていくと、

バグがあります

二点交叉をしている部分と、突然変異の部分です。

ピクセル単位での処理にしようとしている部分で間違えて、

前半4分の1しか触れていません。いやお恥ずかしい

 

という訳で気を取り直して修正し、実行してみました

20170411_ga_01h 20170411_ga_03h 20170411_ga_06h 20170411_ga_10h

なんとなく形が浮かび上がっていますね。

1番上が目指す画像で、

最後の画像は10時間後の結果です

これだけの時間が経ってもまだ少しずつ評価値が上がっていきます。

 

やっとこのGAを使った減色処理のスタートラインです

そう、このままでは全く減色されていませんし、

そもそも全然キレイな画像が生成されていません。

ここからの方針を書いて今回は終わります。

 

.使用する色を1チャンネル16色に制限する

.現在開始画像が完全にランダムな絵だったのを、

 誤差拡散等で既にキレイな状態を出発点にする

.評価式にPSNRを使用しているのを、改良する(SSIM辺り?)

 

おまけ.処理速度を改善する

 →2によって、良い結果が出る時間は短縮されるとは思われますが、

  コアに分散させることで  速度アップが狙えそうな気はします。

 

これでどういった結果になるのか、見ていこうと思います。が、

 

今回はここまで。

では


        

Switchの予約ができてゼルダが待ち遠しい某(なにがし)です
スプラトゥーン2の試射会もとても楽しみにしています

 

 

そういえば今日はバレンタインですね。

 

 

しかし、今回はUnityのプログラムネタです。ごめんなさい。
新しいことではないですが、uGUIでよくある問題の対処法をひとつ紹介します。

 

 

uGUIはとっつきやすく便利ですが、

 

スクロールビュー上のボタン押しにくい(クリックしにくい)

 

という問題に悩まされる人は多いんじゃないかと思います。

 

 

スクロールビュー上のボタンをドラッグするとスクロールが始まりますが、
この時点でボタンに指を離したイベントが発生して押下状態がキャンセルされ、
クリックイベントの対象から外れてしまいます

 

だから、少しでもドラッグするとボタンがクリックできず、押しにくいわけですね・・・

 

マウスならまだマシですが、
これがタッチ操作のときは気になるので、なんとかしたいと思います

 

 

考えられる方針は、

 

  1. ドラッグイベントを開始するドラッグ距離の閾値を与える(ドラッグ判定の遅延)。
  2. ドラッグ開始前に押下していたボタンに、
    ドラッグ終了時にクリックイベントを直接飛ばす(ドラッグで押下状態をキャンセルしない)。

 

こんなところでしょうか。

 

 

1.は一瞬でできます

 

EventSystemコンポーネントのDrag Threshold(pixelDragThreshold)がまさにドラッグ判定の閾値です。
これを大きめの値に設定すれば良いのですが、すべてのドラッグ判定に影響するのがイマイチな感じです

 

それに、スクロールビューのスクロール方向とは異なる向きの移動にも反応するため、
なんだか少しやりたいことと違う気がします

 

 

イベントの発行はBaseInputModuleの仕事であり、
普通なら、その派生クラスであるTouchInputModuleStandaloneInputModuleに任せていると思います。

 

BaseInputModuleを継承して、独自のイベント管理を実装するのも良い気がしますが、
それは仰々しいのでイベント処理側で簡単に対応します

 

 

今回は2.の方針で考えます。

 

 

実装の前に仕組みへの理解を深めるため、まずUnityのイベント処理の流れを簡単に説明します。

 

  1. タッチ入力されると、ポインティングされた座標にカメラからレイキャストを飛ばす。
    (対象はRaycastTargetにチェックを入れたGraphic派生コンポーネントを持つゲームオブジェクト)
  2. レイキャストに一番手前でヒットしたオブジェクトから親オブジェクトへと辿っていき、
    そのときのイベントに対応したイベントハンドラを持つゲームオブジェクトを探す。
    (例えば、OnPointerDownイベントならIPointerDownHandlerが対応)
  3. 見つけたゲームオブジェクトに対してイベント処理のメッセージを送信する。

 

起点となるオブジェクトを決定して、親を辿ってイベントハンドラを探す、
という流れを理解すれば、イベントシステムの使い方が見えてきます。

 

 

これもあるある話だと思うのですが、
スクロールビューの要素となるゲームオブジェクトにEventTriggerを持たせると、
ドラッグイベントも全てここで奪われてスクロールできなくなるといった問題が起きます。
上の処理内容を知らないと混乱しますよね。

 

 

このあたりの挙動はドキュメントでは詳しく説明されていなかったりしますが、
uGUIって実はブラックボックスではないんです。

 

Unity-Technologies / UI — Bitbucket

 

UnityさんがBitbucket上でuGUIのソースコードを公開しています。
何かと困ったらソースコードを見てみるのが早いです。

 

 

実際にソースコードを見てみると、PointerEventDataオブジェクトに
押下中オブジェクト(pointerPress)ドラッグ中オブジェクト(pointerDrag)への参照を持たせて、
これをそのままイベントシステムで利用しているのがわかります。

 

ドラッグイベント処理では、pointerPressとpointerDragが別のオブジェクトだった場合、
pointerPressにOnPointerUpイベントを送信して、参照をnullに差し替えています。

 

とすると、それまでにpointerPressで持っていた参照を独自で記憶するしかないですね

 

 

ということで、ScrollRectを拡張します。(ここまで長かった・・・)

 

  1. OnInitializePotentialDragでpointerPressを記憶
  2. OnEndDragのときの指の位置を見て、改めてレイキャストを飛ばして
    記憶していたpointerPressにヒットすれば、OnPointerClickイベントを送信する。

 

実装例を以下に示します。

 

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public class CustomScrollRect : ScrollRect
{
	public override void OnInitializePotentialDrag(PointerEventData eventData)
	{
		var lastObj = EventSystem.current.currentSelectedGameObject;

		base.OnInitializePotentialDrag(eventData);
		if(!checkValidEvent(eventData)) return;

		// 直前の選択オブジェクトを記憶
		_lastSelectedObj = null;
		if(lastObj != null) {
			_lastSelectedObj = lastObj;
		}
	}

	public override void OnEndDrag(PointerEventData eventData)
	{
		base.OnEndDrag(eventData);
		if(!checkValidEvent(eventData)) return;

		// 直前の選択オブジェクトがあればイベント通知
		if(_lastSelectedObj != null) {
			EventSystem.current.RaycastAll(eventData, _raycastResults);
			var raycast = getFirstRaycast(_raycastResults);
			_raycastResults.Clear();

			notifyClickEvent(raycast, eventData);
		}

		_lastSelectedObj = null;
	}

	protected static RaycastResult getFirstRaycast(IEnumerable results)
	{
		foreach(var result in results) {
			if(result.gameObject != null) {
				return result;
			}
		}
		return new RaycastResult();
	}

	protected void notifyClickEvent(RaycastResult raycast, PointerEventData original)
	{
		if(_lastSelectedObj == null) return;

		var target = raycast.gameObject;
		if(target == null) return;

		// イベントハンドラを取得
		var handler = ExecuteEvents.GetEventHandler(target);
		if(handler == null) return;
		if(handler != _lastSelectedObj) return;

		// 通知用のイベントデータ生成
		var eventData = copyEventData(original);
		eventData.pointerCurrentRaycast = raycast;
		eventData.pointerPressRaycast = raycast;
		eventData.pointerPress = handler;
		eventData.rawPointerPress = target;
		eventData.eligibleForClick = true;

		// イベント実行
		ExecuteEvents.Execute(eventData.pointerPress, eventData, ExecuteEvents.pointerClickHandler);
	}

	protected bool checkValidEvent(PointerEventData eventData)
	{
		if(eventData.button != PointerEventData.InputButton.Left) {
			return false;
		}

		return IsActive();
	}

	protected PointerEventData copyEventData(PointerEventData original)
	{
		return new PointerEventData(EventSystem.current) {
			selectedObject = original.selectedObject,
			hovered = original.hovered,
			button = original.button,
			clickCount = original.clickCount,
			clickTime = original.clickTime,
			delta = original.delta,
			dragging = original.dragging,
			eligibleForClick = original.eligibleForClick,
			pointerCurrentRaycast = original.pointerCurrentRaycast,
			pointerDrag = original.pointerDrag,
			pointerEnter = original.pointerEnter,
			pointerId = original.pointerId,
			pointerPress = original.pointerPress,
			pointerPressRaycast = original.pointerPressRaycast,
			position = original.position,
			pressPosition = original.pressPosition,
			rawPointerPress = original.rawPointerPress,
			scrollDelta = original.scrollDelta,
			useDragThreshold = original.useDragThreshold,
		};
	}

	private static List _raycastResults = new List();

	private GameObject _lastSelectedObj = null;
}

 

使用中のイベントデータを書き換えると不都合がありそうなので、
強引ですが丸ごとコピーしたものを一部書き換えて使ってます。

 

これだけでだいぶボタンを押しやすくなり、押しやすさ改善の基礎はできました。

 

 

あとは、

 

  • ドラッグ開始時にOnPointerUpイベントが呼ばれてしまうのを
    OnPointerClickとタイミングを合わせる
    (ボタンの押下時アニメーションを継続したい)。
  • 一定距離スクロールしたら、記憶したpointerPressを忘れて、
    クリックイベントが飛ばないようにする。

 

このあたり対応すれば、なんだかとっても良い感じになるでしょう。

 

記事が長くなったので、この先の説明は割愛しますが、
今回の話を理解していれば難しい話ではないと思います。

 

 

実は、去年秋にリリースした「アイテム代は経費で落ちない ~no item, no quest~」では、
ここまで手が回らなかったんです。やり残したことのひとつでした・・・

 

このタイトルはあと少しだけアップデート予定があるので、
3月のアップデート内容にスクロールの挙動改善を入れました
アップデートによってどのように変わるか見てみると、なるほどなと思っていただけると思います

 

それでは、
「アイテム代は経費で落ちない ~no item, no quest~」
をよろしくお願いします(宣伝)


ヘキサブログ ピックアップ



過去の日記はこちら

2017年10月
« 9月    
 1
2345678
9101112131415
16171819202122
23242526272829
3031