ヘキサ日記 Blog

 

        

お久しぶりです。
台湾で印象的だった食べ物が鳥の頭と鳥の脚、あとは臭豆腐な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("Prease 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~」
をよろしくお願いします(宣伝)


        

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

今日は東京でも雪が舞うくらい寒くて冬眠したい気分です。

 

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

 

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

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

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

CharacterInfoを生成

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

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

 

 

今回はを説明していきます。

var srcWidth  = _BufferTexture.width;
var srcHeight = _BufferTexture.height;
var dstWidth  = _OutputFontTexture.width;
var dstHeight = _OutputFontTexture.height;

// 文字情報を成形
{
    var charInfo = new CharacterInfo();

    // 文字のインデックスは文字コードのこと
    charInfo.index = (int)writeChar[0];

    // アトラステクスチャ内のUV座標を成形
    // _OutputOffsetXはアトラステクスチャのX座標
    // _OutputOffsetYはアトラステクスチャのY座標
    var x0 = (float)_OutputOffsetX / (float)dstWidth;
    var y0 = (float)_OutputOffsetY / (float)dstHeight;
    var x1 = (float)(_OutputOffsetX + srcWidth) / (float)dstWidth;
    var y1 = (float)(_OutputOffsetY + srcHeight) / (float)dstHeight;
    charInfo.uvBottomLeft  = new Vector2(x0, y0);
    charInfo.uvBottomRight = new Vector2(x1, y0);
    charInfo.uvTopLeft     = new Vector2(x0, y1);
    charInfo.uvTopRight    = new Vector2(x1, y1);

    // ソースの文字情報を取得
    var bearing = 0;
    CharacterInfo srcCharInfo;
    if( _TextArea.font.GetCharacterInfo((char)charInfo.index, out srcCharInfo, _TextArea.fontSize * _Param_QualityScale, _TextArea.fontStyle) ) {
        // UV座標の中で、実際の文字の領域はどこからどこまでか指定する
        // この領域が文字を描画するときに切り出される範囲となる
        charInfo.minX    = (srcCharInfo.minX / _Param_QualityScale) - _Param_PaddingLeft;   // 追加したパディング領域を補正
        charInfo.minY    = (srcCharInfo.minY / _Param_QualityScale) - _Param_PaddingTop;    // 追加したパディング領域を補正
        charInfo.maxX    = (srcCharInfo.maxX / _Param_QualityScale) + _Param_PaddingRight;  // 追加したパディング領域を補正
        charInfo.maxY    = (srcCharInfo.maxY / _Param_QualityScale) + _Param_PaddingBottom; // 追加したパディング領域を補正
        charInfo.advance = (srcCharInfo.advance / _Param_QualityScale) + (_Param_PaddingLeft + _Param_PaddingRight); // 追加したパディング領域を補正
        bearing          = (srcCharInfo.minX / _Param_QualityScale) - _Param_PaddingLeft;
    }
    else {
        charInfo.minX    = 0;
        charInfo.minY    = 0;
        charInfo.maxX    = srcWidth;
        charInfo.maxY    = srcHeight;
        charInfo.advance = srcWidth;
    }

    // ベアリング情報を成形
    var channel      = (BEARING_CHANNEL_TABLE[_OutputPageNo] << 8); // uvのチャンネル情報を間借りしている
    var xoffset      = (bearing + 128); // xoffset値を-128~127を0~255に変換
    charInfo.bearing = channel + xoffset;

    // 文字情報追加
    _OutputFontModel._chars._charInfos.Add(charInfo);
}

 

8行目でCharacterInfoを作成して情報を入れていきます。

 

28行目で _TextArea(Textコンポーネント)から情報を取得します。

 

31~35行目ではパディング領域(文字の周りの隙間)を計算しています。

 

47~49行目ではベアリング値を設定しているのですが、このベアリング値に独自の情報を持たせています。

 

ベアリングについてはこちら

 

今回は、出力するアトラステクスチャのRGBAチャンネルを別々に使用してフォント情報を格納しています。

DistanceFieldTexture は白黒情報なので4チャンネルを有効に使ってアトラステクスチャが大きくなるのを防いでいます。

第2水準までのフォントで1024×1024のRGBAテクスチャ1枚に収まりました。

 

そこで、必要な文字がどのチャンネルに入っているかという情報が必要になる訳です。

ベアリングの上位8ビットにチャンネルのインデックス0~3を格納し、下位8ビットに元々のベアリング情報を0~255に変換して格納しています。

本来ベアリングはマイナス値も格納できますが、下位8ビットで表現するために0~255に変換しています。

もちろん、このフォントを描画するシェーダー側にベアリングを再変換する処理を入れておかないと、うまく描画されません。

 

最後に52行目で、_OutputFontModel(FontModelクラス)に格納します。

 

 

今回はここまで。以降はまた次回のお楽しみに


        

「花粉シーズン突入、予想飛散量は約1.2倍」というニュースを見て、

もう外に出たくないなぁ~なんて思い始めてます。

こんにちは、だっちです

花粉症の人は一刻も早く対策を始めたほうが良いですよ

 

 

突然ですが、ゲームを作るときに必要になるデータってたくさんありますよね。

その中でも敵とか武器とかアイテムとかのパラメーター、何で管理していますか?

社内では、データを入力する企画の方々が使い慣れているExcelを使う事が多いです。

 

 

今回は、Excelで作成した各種データを、

ゲーム内で扱いやすい形に変換するためのプログラムを、

 ・ゲーム内で扱うための型を自動的に生成

 ・型を生成するための情報はExcel内に定義

できるように、作っていこうと思います。

 

 

Excelファイルを扱うためのライブラリは色々ありますが、

今回は「NPOI」というライブラリをC#経由で使ってみようと思います。

 

 

早速、NPOIを使用してExcelファイルの中身を取得していこうと思います。

適当なExcelファイルを作成して、

適当にC#プロジェクトを作成して、DLしてきたNPOIを参照に加えておきます。

 

 

今回はこんな感じのExcelデータを用意しました。

NPOITestExcelDataSample

 

 

NPOIを使用してExcelファイルを読み込むためのコードはこんな感じ

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

namespace NPOITest
{
	class Program
	{
		// エントリポイント
		public static void Main(string[] args)
		{
			// 入力受付
			Console.Write("Prease 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);

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

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

					// A1セルの内容を読み込み
					// まずは行を取得
					var row = sheet.GetRow(0);
					if( row == null ) continue;

					// 行からセルを取得
					var cell = row.GetCell(0);
					if( cell == null ) continue;

					// セルが取得できたら表示
					Console.WriteLine("		A1 : " + cell.ToString());
				}
			}
		}
	}
}

 

 

実行結果

NPOITestExecResult001

中にあるシートとセルに入力されている文字列が取得できていますね

 

 

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

ではでは~

 

 


2017年1月11日

32とか16とかその6

        

こんにちは。

 

シュンスケです

 

前回、どれがキレイ(元画像に近い)なのかをプログラムで判断できれば、

数あるパターンから最適なものが見つけられるのではといった話がありました

 

そこで、今回のアプローチは、GA

 

ガーディアンエンジェルではありません。

ギャラクシーエンジェルでもありません。

 

Genetic Algorithmつまり、「遺伝的アルゴリズム」です

 

大まかに説明すると、とある解を求めるにあたって、生物の進化のように、

解候補たちを組み替えながら、正解に近いものは残して、遠いものを淘汰し、

時には突然変異を起こすといった感じで、多くのパターンからより良いものを

導き出す処理方法です

 

と、ここまで来て気付く方も多いと思いますが、正解に近いかどうか、それが今回でいうと、

元画像に近いかどうかということです。

 

その比較方法でお手軽なのがPSNR値を使うものなようなので、今回はそうします。

(ただし、人間の目を相手にするとなると、問題は多いようです)

 

そして今回、果敢にも全ピクセルの全チャンネルに関して、組み換え対象として

実装しました。

つまり、膨大なパターンがあるということです

 

さて、以下実装です。

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
		{
			public static void cross(ref Gene a, ref Gene b)
			{
				if( a.buf == null || b.buf == null ) return;

				// 2点交叉
				var r = new Random();
				var index1 = r.Next(0, (a.buf.Length - 1) / PIXEL_BYTES);
				var index2 = r.Next((index1 + 1) / PIXEL_BYTES, b.buf.Length / PIXEL_BYTES);
				for( int i = index1; i < index2; ++i ) {
					var tmp0 = a.buf[i + 0];
					var tmp1 = a.buf[i + 1];
					var tmp2 = a.buf[i + 2];
					var tmp3 = a.buf[i + 3];
					a.buf[i + 0] = b.buf[i + 0];
					a.buf[i + 1] = b.buf[i + 1];
					a.buf[i + 2] = b.buf[i + 2];
					a.buf[i + 3] = b.buf[i + 3];
					b.buf[i + 0] = tmp0;
					b.buf[i + 1] = tmp1;
					b.buf[i + 2] = tmp2;
					b.buf[i + 3] = tmp3;
				}
			}

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

			// ランダムで埋める
			public void fillRand()
			{
				// ランダムで色を埋める
				var r = new Random();
				r.NextBytes(buf);
			}

			// PSNRを算出して保持
			public void calcPSNR(byte[] org)
			{
				psnr = -1;
				if( org == null || buf == null || org.Length != buf.Length || org.Length == 0 ) return;

				// MSE算出
				ulong mse = 0;
				for( int i = 0; i < org.Length; ++i ) {
					long d = (buf[i] - org[i]);
					mse += (ulong)(d * d);
				}
				mse /= (ulong)org.Length;
				if( mse <= 0 ) return;

				// PSNR算出
				psnr = (10 * Math.Log10((double)(255 * 255) / (double)mse));
			}

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

				var r = new Random();
				var index = r.Next(0, buf.Length / PIXEL_BYTES);
				buf[index + 0] = (byte)r.Next(0, 256);
				buf[index + 1] = (byte)r.Next(0, 256);
				buf[index + 2] = (byte)r.Next(0, 256);
				buf[index + 3] = (byte)r.Next(0, 256);
			}

			// 値を設定
			public void set(Gene g)
			{
				if( g.buf == null ) return;

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

				Array.Copy(g.buf, buf, buf.Length);
				psnr = g.psnr;
			}

			public byte[]	buf;
			public double	psnr;
		}

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

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

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

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

				// 解放
				img.UnlockBits(dat);
			}
			if( buf == null ) {
				Console.WriteLine("buf load failed.");
				return;
			}

			// フォーム開始
			Application.Run(new Program(ref buf, width, height));
		}

		// コンストラクタ
		public Program(ref byte[] buf, int w, int h)
		{
			if( buf == null ) return;

			_org = buf;
			_buf = new byte[buf.Length];
			_img = new Bitmap(w, h);

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

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

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

			// 描画更新開始
			_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( _buf != null ) {
				var newDat = _img.LockBits(new Rectangle(Point.Empty, _img.Size), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
				Marshal.Copy(_buf, 0, newDat.Scan0, _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, 100, 0);
		}

		// 描画更新
		private void refresh(object sender, EventArgs e)
		{
			// 保持したトップを描画バッファに設定
			if( _topGen.buf != null ) {
				Array.Copy(_topGen.buf, _buf, _buf.Length);
			}

			// 画面をリフレッシュ
			++_sec;
			Invalidate();
		}

		// 処理更新
		private void update(object sender, EventArgs e)
		{
			// 評価値算出
			for( int i = 0; i < _curGen.Length; ++i ) {
				_curGen[i].calcPSNR(_org);
			}

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

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

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

			// 上位いくつかはそのまま残す
			var newIndex = 0;
			for( int i = 0; i < 2; ++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[]	_buf			= null;

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

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

 

このままでは処理速度に難がありそうですが、実行してみます

 

ウィンドウの中で、上が目指す画像で、下が完全ランダム値からはじめて、

最終的には同じ絵になって欲しい部分です。

20170111_01

はじめの方

20170111_02

 

約50分後

 

う、うーん…ダメだ

さすがにパターンが多すぎたようです。

それに、交叉や突然変異の確率、評価処理もかなー、 見直しが必要そうです。

というか、そもそも全ピクセルランダムスタートで辿り着けるのか。。。

 

ただ、薄っすらと緑っぽく近づいてきてはいるので、アプローチの方向はまだ捨てずに

おこうと 思いますが、今回はここまで

 

では


2016年12月2日

優しいC++解析(その2)

        

ついに今年も最後の月になりましたね。
ここにきて期待のホラー映画ブレアウィッチまで登場してワクワクが止まりません。
お久しぶりです、good sunこと山口です。

さて、今回は前回に引き続いてclangを使ったC++のソース解析の続きを行います。

前回ではクラスや構造体の情報を解析するコードについて簡単に触れました。

今回はそこから発展して、特定の変数について情報を追加してみたいと思います。

具体的には以下の様なコードで

class Sample{
public:
	Sample();
	~Sample();
private:
	PARAM(_SERIALIZE("no"), _HINT("hint text"))
	long		_value;
};

_valueという変数に対して

・シリアライズを行わない

・ヒント情報としてhint textという情報を持つ

という情報を付与する事を目標とします。

その前にNuGetで取得できるClangSharpの不具合を修正します。

clang.parseTranslationUnit2の引数でUnsavedFileをout属性で渡すという処理になっていますが、

こちらは配列を渡すのが本来の挙動になります。

このUnsavedFileはメモリ上のファイルを読み込ませたりする目的で使うのでout属性ではそれが達成出来ません。

まずは開発者のGitHubのページを見てみます。(オープンソースって素晴らしいですね)

https://github.com/Microsoft/ClangSharp

見てみると実はその辺対策済みなコードが上がっています。

のでこちらをcloneするかdownloadします。

(私はGitHub Desktopがエラーになったので男らしくdownloadを選びました)

20161202_00

downloadもしくはcloneしたファイルにはClangSharp.slnが入っているのでコレを開きます。

※公式ページのBuilding ClangSharpの手順に従うと修正されているハズのファイルが上書きされてしまうので、

公式に反旗を翻すアウトローになり切ってください

次にExtensions.csを開いて8行目をコメントアウトします。

※時間的に原因調査していませんが…そのままではここのdisposeによってハングしてしまいます。

コメントアウトが完了したらソリューションエクスプローラから

ClangSharpのプロジェクトを右クリックでビルドします。

20161202_01

出来上がったClangSharp.dllをpdbと共にNuGetで追加したプロジェクトの

package\ClangSharp.3.8.0\lib\net40に上書きします。

念のためClangSharpがclang3.9対応版との事なのでlibclang.dllも3.9のものに差し替えます。

llvmをビルドすると大変なのでClang for Windowsをインストールします。

http://llvm.org/releases/download.html

20161202_02

インストール先のLLVM\binの下のlibclang.dllを

packages\ClangSharp.3.8.0\content\x64に上書きコピーします。

因みにこの状態になるまで主に例のdisposeの所で原因不明の停止を繰り返すことになりましたが

対策としてClangSharpを使うプロジェクトのデバッグ設定で

ネイティブコードデバッグを有効にするのチェックを入れ忘れていて調査に手間取ったという事があったので、

この項目へのチェックは推奨です

20161202_03

はい、準備が整いましたので本題です。

流れとしましてはUnrealEngineでやっているようなマクロを作ってそれを活用することになります。

ちょっと今回手抜きをしてマクロ全部入りのコードで紹介をしますが…

C++のコードはこんなコードを使います。

#define CLASS_DEFS()	\
					virtual void serialize();	\
					virtual void deserialize();	\

#define _HINT(n) const char* hint=n
#define _SERIALIZE(n) const char* serialize=n

#define _COMBINE__(a,b)	a##b
#define _COMBINE_(name,l)	_COMBINE__(name, l)
#define PARAM(...)	void _COMBINE_(__Paramater_, __LINE__)(__VA_ARGS__);

namespace test{

class Sample{
public:
	Sample();
	~Sample();
private:
	PARAM(_SERIALIZE("no"), _HINT("hint text等"))
	long		_value;
};

}

CLASS_DEFS()の定義はゲームとしてビルドするときは空になる様にifdefで分けるようにする必要があります。

今回は仕組みの説明なので…

解析時はこのマクロが出てきたところに関数宣言を展開してキーワードとしてClangから抽出を行おうと考えています。

さらにデフォルト引数を使って入力パラメータの解釈を行います。

前回の解析用コードを少し変更してこんな感じのコードを使って解析します。

using System;
using System.Collections.Generic;
using System.Text;
using ClangSharp;

namespace ClangTest
{
	// パラメータ解析用クラス
	public class Param
	{
		public bool Serialize { get; set; } = true;
		public string Hint { get; set; }
		public void parseFunc(CXCursor cursor)
		{
			var param = clang.getCursorSpelling(cursor).ToString();
			switch (param) {
			case "serialize":
				clang.visitChildren(cursor, new CXCursorVisitor(visitSerialize), new CXClientData());
				break;
			case "hint":
				clang.visitChildren(cursor, new CXCursorVisitor(visitHint), new CXClientData());
				break;
			}
		}
		// シリアライズの有無のチェック
		CXChildVisitResult visitSerialize(CXCursor cursor, CXCursor parent, IntPtr client_data)
		{
			if (clang.getCursorKind(cursor) == CXCursorKind.CXCursor_StringLiteral)
			{
				var val = clang.getCursorSpelling(cursor).ToString().Replace("\"","");
				switch (val.ToLower()) {
				case "no":
				case "false":
					Serialize = false;
					break;
				default:
					break;
				}
				return CXChildVisitResult.CXChildVisit_Break;
			}
			return CXChildVisitResult.CXChildVisit_Recurse;
		}
		// ヒントテキストを取得
		CXChildVisitResult visitHint(CXCursor cursor, CXCursor parent, IntPtr client_data)
		{
			if (clang.getCursorKind(cursor) == CXCursorKind.CXCursor_StringLiteral) {
				var val = clang.getCursorSpelling(cursor).ToString();
				val = val.Substring(1, val.Length - 2);
				Hint = val;
				return CXChildVisitResult.CXChildVisit_Break;
			}
			return CXChildVisitResult.CXChildVisit_Recurse;
		}
		public override string ToString()
		{
			return string.Format("serialize={0},hint={1}", Serialize, Hint);
		}
	}

	class Program
	{
		static void Main(string[] args)
		{
			var index = clang.createIndex(0, 0);
			var ufile = new CXUnsavedFile();
			var trans = new CXTranslationUnit();
			ufile.Filename = "dummy.cpp";
			ufile.Contents = "#include \"sample.h\"";
			ufile.Length = ufile.Contents.Length;

			var erro = clang.parseTranslationUnit2(index, "dummy.cpp", new String[] { "-std=c++11" }, 1, new CXUnsavedFile[] { ufile }, 1, (uint)(ClangSharp.CXTranslationUnit_Flags.CXTranslationUnit_SkipFunctionBodies), out trans);
			var cursor = clang.getTranslationUnitCursor(trans);
			var kind = clang.getCursorKind(cursor);
			var pg = new Program();
			clang.visitChildren(cursor, new CXCursorVisitor(pg.visitChild), new CXClientData());

		}
		protected List _namespace = new List();
		protected String _namespaceStr = "";
		protected Dictionary<uint, Param> _params = new Dictionary<uint, Param>();
		CXChildVisitResult visitChild(CXCursor cursor, CXCursor parent, IntPtr client_data)
		{
			switch (cursor.kind) {
			case CXCursorKind.CXCursor_Namespace:
				readNamespace(cursor);
				clang.visitChildren(cursor, new CXCursorVisitor(visitChild), new CXClientData());
				removeNamespace();
				break;
			case CXCursorKind.CXCursor_ClassDecl:
				readClass(cursor);
				break;
			case CXCursorKind.CXCursor_EnumDecl:
				readEnum(cursor);
				break;
			case CXCursorKind.CXCursor_FieldDecl:
				readField(cursor);
				break;
			case CXCursorKind.CXCursor_ClassTemplate:
				readTClass(cursor);
				break;
			case CXCursorKind.CXCursor_CXXMethod:
				readMethod(cursor);
				break;
			default:
				break;
			}
			return CXChildVisitResult.CXChildVisit_Continue;
		}
		CXChildVisitResult visitEnum(CXCursor cursor, CXCursor parent, IntPtr client_data)
		{
			var itype = clang.getEnumDeclIntegerType(cursor);
			if (itype.kind == CXTypeKind.CXType_UInt) {
				Console.WriteLine("{0} {1}", clang.getCursorSpelling(cursor), clang.getEnumConstantDeclUnsignedValue(cursor));
			}
			else {
				Console.WriteLine("{0} {1}", clang.getCursorSpelling(cursor), clang.getEnumConstantDeclValue(cursor));
			}
			return CXChildVisitResult.CXChildVisit_Continue;
		}

		void readNamespace(CXCursor cursor)
		{
			var cxName = clang.getCursorSpelling(cursor);
			String name = cxName.ToString();
			_namespace.Add(name);
			_namespaceStr = String.Join("::", _namespace);
		}
		void removeNamespace()
		{
			_namespace.RemoveAt(_namespace.Count - 1);
			_namespaceStr = String.Join("::", _namespace);
		}
		void readClass(CXCursor cursor)
		{
			Console.WriteLine("クラス定義発見 {1}::{0}", clang.getCursorSpelling(cursor), _namespaceStr);
			// さらに子を検索する
			clang.visitChildren(cursor, new CXCursorVisitor(visitChild), new CXClientData());
			_params.Clear();
		}
		void readTClass(CXCursor cursor)
		{
			Console.WriteLine("テンプレートクラス定義発見 {0}", clang.getCursorSpelling(cursor));
			// さらに子を検索する
			clang.visitChildren(cursor, new CXCursorVisitor(visitChild), new CXClientData());
		}
		void readEnum(CXCursor cursor)
		{
			Console.WriteLine("enum定義発見 {0}", clang.getCursorSpelling(cursor));
			// さらに子を検索する
			clang.visitChildren(cursor, new CXCursorVisitor(visitEnum), new CXClientData());
		}
		void readMethod(CXCursor cursor)
		{
			var name = clang.getCursorSpelling(cursor).ToString();
			// パラメータ解析する為のキーワードを見つけたら
			if (name.StartsWith("__Paramater_"))
			{
				var location = clang.getCursorLocation(cursor);
				CXFile file;
				uint line;
				uint column;
				uint offset;
				// 行番号とセットで情報を覚えさせる
				clang.getSpellingLocation(location, out file, out line, out column, out offset);
				Console.WriteLine("パラメータ宣言発見 {0} {1}", name, line);
				var param = new ClangTest.Param();
				// これで引数を解析
				var n = clang.Cursor_getNumArguments(cursor);
				for (uint i = 0; i < n; ++i)
				{
					var arg = clang.Cursor_getArgument(cursor, i);
					param.parseFunc(arg);
				}
				_params.Add(line + 1, param);
			}
		}
		void readField(CXCursor cursor)
		{
			var type = clang.getCursorType(cursor);
			String typeName;
			var arrCount = clang.getArraySize(type);
			if (arrCount > 0) {
				var arrType = clang.getArrayElementType(type);
				typeName = String.Format("{0}=>({1})", arrType, arrCount);
			}
			else {
				typeName = type.ToString();
			}
			var location = clang.getCursorLocation(cursor);
			CXFile file;
			uint line;
			uint column;
			uint offset;
			// フィールドが出てきたときに覚えていた行数とパラメータ情報に一致するものがあったらその情報を適応する
			clang.getSpellingLocation(location, out file, out line, out column, out offset);
			Param pm;
			if (_params.TryGetValue(line, out pm)) {
				Console.WriteLine("パラメタ付変数宣言発見 {0} {1} {2}", clang.getCursorSpelling(cursor), typeName, pm);
			}
			else {
				Console.WriteLine("変数宣言発見 {0} {1}", clang.getCursorSpelling(cursor), typeName);
			}
		}
	}
}

という感じで、

特定の名前の関数が登場した行数とそこから取得できるデフォルト引数を記録して、

フィールドが出てきたときに行数をチェックして該当のパラメータを持っているという解釈をさせるという事で

フィールドに情報の追加を行う事が出来ます。

デフォルト引数の取得にちょっと苦戦しましたが…思いの外すんなり出来てしまうものですね

ここまでくるとそろそろ次回辺りは実践編になるかもですね。

という事で今回はここまでです。

ではまた次回にお会いしましょう


2016年12月1日

C#のLINQが便利!

        

シワッスプログラマーの平尾です。

 

今回はプログラマー向けの話です。
C#3.0からLINQという凄い便利な機能が使えるので、簡単に紹介します。

 

LINQで一体何が出来るのかというと、
「コレクションの操作を非常に簡潔に記述すること」
が出来ます。

 

と言われてもLINQを知らない方はピンと来ないと思いますので、
サンプルプログラムを書いてみました。
(LINQはメソッド構文で記述しています)

 

【ソース】

using System;
using System.Linq;    // LINQを使用する際に必要なusing

class Program
{
    public class Data
    {
        public string    name;
        public string    home;
        public int       power;
    }

    static void Main(string[] args)
    {
        // キャラクターデータ
        var src = new Data[]
        {
            new Data { name = "筋肉男", home = "日本",     power = 95   },
            new Data { name = "星肩男", home = "アメリカ", power = 95   },
            new Data { name = "鉄仮面", home = "イギリス", power = 95   },
            new Data { name = "拳法男", home = "中国",     power = 97   },
            new Data { name = "軍服男", home = "ドイツ",   power = 90   },
            new Data { name = "熊爪男", home = "ロシア",   power = 100  },
            new Data { name = "猛牛男", home = "スペイン", power = 1000 },
            new Data { name = "元人間", home = "アメリカ", power = 83   },
        };

        // 名前だけ取り出す
        var dst = src.Select( x => x.name );
        Console.WriteLine("■名前一覧");
        foreach ( var data in dst ) {
            Console.WriteLine("name = {0}", data);
        }

        // パワーが100以上のキャラクターを取得
        var dst2 = src.Where( x => x.power >= 100 );
        Console.WriteLine("\n■パワーが100以上のキャラクター");
        foreach( var data in dst2 ) {
            Console.WriteLine("power = {0, 4}, name = {1}", data.power, data.name);
        }

        // 日本またはアメリカ出身のキャラクター数
        int count = src.Count( x => x.home == "日本" || x.home == "アメリカ" );
        Console.WriteLine("\n■日本/アメリカ出身者数 = {0}人", count);

        // パワーが高い順にソート
        var dst3 = src.OrderByDescending( x => x.power );
        Console.WriteLine("\n■パワーが高い順");
        foreach( var data in dst3 ) {
            Console.WriteLine("power = {0, 4}, name = {1}", data.power, data.name);
        }

        // 出身地でグループ化する
        var dst4 = src.GroupBy(x => x.home, x => x.name );
        Console.Write("\n■出身地別");
        foreach (var group in dst4) {
            Console.Write("\n{0}:", group.Key);
            foreach (var name in group) {
                Console.Write("{0}, ", name);
            }
        }
    }
}

 

【出力結果】

■名前一覧
name = 筋肉男
name = 星肩男
name = 鉄仮面
name = 拳法男
name = 軍服男
name = 熊爪男
name = 猛牛男
name = 元人間

■パワーが100以上のキャラクター
power =  100, name = 熊爪男
power = 1000, name = 猛牛男

■日本/アメリカ出身者数 = 3人

■パワーが高い順
power = 1000, name = 猛牛男
power =  100, name = 熊爪男
power =   97, name = 拳法男
power =   95, name = 筋肉男
power =   95, name = 星肩男
power =   95, name = 鉄仮面
power =   90, name = 軍服男
power =   83, name = 元人間

■出身地別
日本:筋肉男,
アメリカ:星肩男, 元人間,
イギリス:鉄仮面,
中国:拳法男,
ドイツ:軍服男,
ロシア:熊爪男,
スペイン:猛牛男, 

 

プログラム中で以下のように呼び出しているのがLINQクエリです。

src.Where()
src.Select()
src.Count()
src.OrderByDescending()
src.GroupBy()

 

本来ならループして書くところが、とんでもなく簡潔に書けていると思います。
データを取り出す処理はどれも1行
むしろ表示部分の方が長いという…

 

 

この他にも様々な機能が存在していますし、
次のようにLINQクエリを重ねて書くこともできます。

src.Where.().Select()

 

LINQはこのように複雑な処理を簡潔に記述でき、とても便利です

気になった方はネットで詳しい使用方法を検索してみて下さい~


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



過去の日記はこちら

2017年8月
« 7月    
 123456
78910111213
14151617181920
21222324252627
28293031