text_edit_object_with_tkinter.py を Blender 2.62 で動かす

リソース・チュートリアル : 最近の Blender 2.5の Text オブジェクトの日本語対応 Blender.jp
この記事で紹介されているスクリプトを修正し 2.62 で動作するところまでこぎつけました。
動作確認をしたのは Windows XP 32bit、Windows 7 64bit のみです。
Python は 3.2 です。


text_edit_object_with_tkinter_2_62.py
※この記事の最後にスクリプトの全文を記載しています。

インストール

Python のパスがうまく解決できなかったため、とりあえずファイルをコピーして動作するのを確認しています。このあたりは出来れば解決したい……
次のファイルを Blenderpython ディレクトリにコピーしてください。
(\Blender Foundation\Blender\2.62\python 以下)

  • Python32\DLLs
  • Python32\tcl
  • Python32\Lib\tkinter


スクリプトは普通にアドオンのインストールの手順でインストールしてください。
アドオンのカテゴリーは Text です。
名称は Edit text object with Tkinter となっていて元のスクリプトから変更しておりません。

使用方法

ウィンドウを表示するには、Text Object を追加し、選択し、Properties -> Object Data を選択し、一番下の Edit Text Object ボタンをクリックしてください。
Load Fonts ボタンの方は何も変更を加えていないので動かないかもしれません。
その場合フォントは適切なフォントを選択しておいてください。
デフォルトのフォントでは日本語が入っていない(?)ため、日本語の文字が表示されません。


ウィンドウを表示したときにランタイムエラーが発生する場合、次のファイルをリネーム等して読み込まれない状態にすればエラーは表示されなくなると思います。
\Blender Foundation\Blender\msvcr90.dll


ウィンドウが表示されている状態でも Blender 上で操作は可能です。
ただし、ウィンドウを表示するときに選択されていたオブジェクトを参照しているので、別の Text Object を選択しテキストを編集しても最初に選択していた Text Object に変更が反映されます。
また、最初に選択していた Text Object を削除した場合正しく動作しません。


ウィンドウ内でテキストを書き換えるとリアルタイムに 3D View 上の Text Object に反映します。
Apply ボタンを押下するか、×ボタンでウィンドウを閉じて編集を完了してください。
Cancel ボタンを押下すると、ウィンドウを立ち上げたときの内容のテキストに差し戻します。

text_edit_object_with_tkinter_2_62.py

# coding: utf-8

# ##### BEGIN GPL LICENSE BLOCK #####
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####


bl_info = {
    'name': 'Edit text object with Tkinter',
    'author': 'chromoly',
    'version': (0, 0, 1),
    'blender': (2, 6, 2),
    'api': 32385,
    'location': 'Properties -> ObjectData',
    'description': '',
    'warning': '',
    'category': 'Text'}

'''
日本語入力の為、TkinterのGUIを呼び出します。GUIを呼び出している間、Blenderは止まります。
参考: http://docs.python.org/py3k/library/tkinter.html
      Pythonプログラミング入門

'''

import os
import sys
import threading

#### 環境に合わせて修正してください #############################################
## Tkinter path
'''
importエラーを吐くようなら、モジュールパスを修正してください。
'''
try:
    import tkinter as tk
except:
    if sys.platform == 'win32':  # Windows
        sys.path.append('C:\\Python32\\Lib')
        sys.path.append('C:\\Python32\\DLLs')
        os.environ['TCL_LIBRARY'] = 'C:\\Python32\\tcl\\tcl8.5'
    else:  # sys.platform == 'linux2'  etc...
        prefix = '/usr'
        dirname = 'python' + sys.version[:3]  # python3.1
        sys.path.append(os.path.join(prefix, 'lib', dirname))
        sys.path.append(os.path.join(prefix, 'lib', dirname, 'lib-dynload'))

## Load Fonts
'''
'Load Fonts'ボタンで複数のフォントを一度に読み込みます。
'''
if sys.platform == 'win32':  # Windows
    font_paths = ('C:\WINDOWS\Fonts\msgoth04.ttc',
                  'C:\WINDOWS\Fonts\msmin04.ttc')
else:  # Linux etc.
    font_paths = ('/home/hoge/.fonts/meiryo/meiryo.ttc',
                  '/home/hoge/.fonts/meiryo/meiryob.ttc')

## Tkinter GUI
'''
FONTNAMEは、利用可能なフォント名を指定してください。
読み込めない場合はデフォルトのフォントが使われます。
'''
FONTNAME = 'Meiryo'
FONTSIZE = 11
###############################################################################

import tkinter as tk
from tkinter.scrolledtext import ScrolledText
from tkinter.filedialog import askopenfilename, asksaveasfilename
import bpy
from bpy.props import *
import time

class Scroller(tk.Frame):
    def __init__(self, master=None, textobj=None):
        tk.Frame.__init__(self, master)
        self.pack()
        self.cached_text = ""

        # <Title>
        master.title('Edit TextObject')

        # <Menu>
        self.mb = tk.Menu(master, font=(FONTNAME, FONTSIZE))

        self.fm = tk.Menu(self.mb, font=(FONTNAME, FONTSIZE), tearoff=0)
        self.fm.add_command(label='開く', command=self.openFile)
        self.fm.add_command(label='別名で保存', command=self.saveFile)
        self.fm.add_separator()
        self.fm.add_command(label='終了', command=self.quit)  # master.quit
        self.mb.add_cascade(label='ファイル', menu=self.fm)

        self.em = tk.Menu(self.mb, font=(FONTNAME, FONTSIZE), tearoff=0)
        self.em.add_command(label='切り取り', command=self.cutSelection)
        self.em.add_command(label='複製', command=self.copySelection)
        self.em.add_command(label='貼り付け', command=self.pasteClip)
        self.mb.add_cascade(label='編集', menu=self.em)

        master.config(menu=self.mb)

        # <Shortcut>  master.bindだと二回繰り返す事に
        self.bind('<Control-KeyPress-x>', self.cutSelection)
        self.bind('<Control-KeyPress-c>', self.copySelection)
        self.bind('<Control-KeyPress-v>', self.pasteClip)

        # <Button>
        f1 = tk.Frame(master, relief=tk.SUNKEN, bd=1)
        f1.pack()
        self.bpy_clear = tk.Button(f1, text='Clear', \
                                   font=(FONTNAME, FONTSIZE), \
                                   command=self.bpyClear, width=12)
        self.bpy_clear.pack(side='left')
        '''
        self.bpy_read = tk.Button(f1, text='Read', \
                                  command=self.bpyRead, width=12)
        self.bpy_read.pack(side='left')
        self.bpy_apply = tk.Button(f1, text='Apply', width=12)
        self.bpy_apply['command'] = self.bpyApply  # こんな書き方も
        self.bpy_apply.pack(side='left')
        '''
        self.bpy_applyquit = tk.Button(f1, text='Apply', \
                                       font=(FONTNAME, FONTSIZE),\
                                       command=self.bpyApplyQuit, width=12)
        self.bpy_applyquit.pack(side='left')

        self.bpy_quit = tk.Button(f1, text='Cancel', \
                                  font=(FONTNAME, FONTSIZE), \
                                  command=self.bpyCancel, width=12)
        self.bpy_quit.pack(side='right')

        # <Text>
        self.st = ScrolledText(master, font=(FONTNAME, FONTSIZE))
        self.st.pack(fill=tk.BOTH, expand=1)
        self.st.focus_set()
        
        # <Read from bpy.context.active_object>
        self.setTextObject(textobj)

    def setTextObject(self, txtobj):
        self.text_active_object = txtobj
        
    def getTextObject(self):
        ob = self.text_active_object
        if ob and ob.type == 'FONT':
            return ob
        return None

    def bpyClear(self):
        self.st.delete('1.0', tk.END)

    def bpyRead(self):
        ob = self.getTextObject()
        if ob:
            editmode = ob.mode
            if editmode == 'EDIT':
                bpy.ops.object.mode_set(mode='OBJECT')
            text = ob.data.body
            self.cached_text = text
            self.st.delete('1.0', tk.END)
            self.st.insert(tk.END, text)
            if editmode == 'EDIT':
                bpy.ops.object.mode_set(mode='EDIT')

    def setText(self, str):
        ob = self.getTextObject()
        if ob:
            editmode = ob.mode
            if editmode == 'EDIT':
                bpy.ops.object.mode_set(mode='OBJECT')
            ob.data.body = str
            if editmode == 'EDIT':
                bpy.ops.object.mode_set(mode='EDIT')        
                
    def bpyApply(self):
        self.setText(self.st.get('1.0', tk.END))

    def bpyApplyQuit(self):
        self.bpyApply()
        self.quit()
        
    def bpyCancel(self):
        self.setText(self.cached_text)
        self.quit()

    def openFile(self):
        fn = askopenfilename()
        if fn:
            fi = open(fn, 'r')
            b = fi.read()
            fi.close()
            self.st.delete('1.0', tk.END)
            self.st.insert(tk.END, b)

    def saveFile(self):
        fn = asksaveasfilename()
        if fn:
            fo = open(fn, 'w')
            fo.write(self.st.get('1.0', tk.END))
            fo.close()

    def cutSelection(self, *tmp):
        if self.st.tag_ranges(tk.SEL):
            self.copySelection()
            self.st.delete(tk.SEL_FIRST, tk.SEL_LAST)

    def copySelection(self, *tmp):
        if self.st.tag_ranges(tk.SEL):
            t = self.st.get(tk.SEL_FIRST, tk.SEL_LAST)
            self.st.clipboard_clear()
            self.st.clipboard_append(t)

    def pasteClip(self, *tmp):
        if self.st.tag_ranges(tk.SEL):
            self.st.delete(tk.SEL_FIRST, tk.SEL_LAST)
        t = self.st.selection_get(selection='CLIPBOARD')
        self.st.insert(tk.INSERT, t)

class Invoker(threading.Thread):
    def __init__(self, m):
        threading.Thread.__init__(self)
        self.method = m

    def run(self):
        self.method()

class TEXT_OT_edit_object_with_tkinter(bpy.types.Operator):
    '''Call Tkinter GUI'''
    bl_description = 'Call Tkinter GUI and Blender stop'
    bl_idname = 'text.edit_object_with_tkinter'
    bl_label = 'Call Tkinter GUI'

    @classmethod
    def poll(cls, context):
        actob = context.active_object
        return actob and actob.type == 'FONT'

    def pre_call(self):
        self.root = tk.Tk()
        self.app = Scroller(master=self.root, textobj=bpy.context.active_object)
        
        self.app.st.bind("<ButtonRelease>", self.callback)
        self.app.st.bind("<KeyRelease>", self.callback)
        
    def callback(self, event):
        self.app.bpyApply()
        
    def call_tk(self):
        self.app.bpyRead()
        self.app.mainloop()
        try:
            self.root.destroy()
        except:
            pass
        del self.app
        del self.root
        del self.t
        self._timer = bpy.context.window_manager.event_timer_add(0.1, bpy.context.window)
            
    def execute(self, context):
        context.window_manager.modal_handler_add(self)        
        self.pre_call()        
        self.t = Invoker(self.call_tk)
        self.t.start()        
        return {'RUNNING_MODAL'}
        
    def cancel(self, context):
        return {'CANCELLED'}

    def modal(self, context, event):
        if event.type == 'ESC':  # Cancel
            return self.cancel(context)
        if event.type == 'TIMER':
            context.window_manager.event_timer_remove(self._timer)
            del self._timer
            return {'CANCELLED'}
        
        return {'PASS_THROUGH'}


class TEXT_OT_load_fonts(bpy.types.Operator):
    '''Load fonts'''
    bl_description = 'Load fonts. ' + \
                   '(edit "text_edit_object_with_tkinter.py":62~76:font_paths)'
    bl_idname = 'text.load_fonts'
    bl_label = 'Load Fonts'

    setactob = BoolProperty(default=False)

    '''
    @classmethod
    def poll(cls, context):
        actob = context.active_object
        return actob and actob.type == 'FONT'
    '''

    def execute(self, context=None):
        if not font_paths:
            return {'FINISHED'}
        loadedpaths = [f.filepath for f in bpy.data.fonts]
        for path in font_paths:
            if not path in loadedpaths:
                bpy.ops.font.open(filepath=path)

        if self.setactob:
            actob = bpy.context.active_object
            if actob and actob.type == 'FONT':
                for font in bpy.data.fonts:
                    if font.filepath == font_paths[0]:
                        actob.data.font = font
                        break
        return {'FINISHED'}


class DATA_PT_call_tkinter(bpy.types.Panel):
    bl_label = "Edit Text with Tkinter"
    COMPAT_ENGINES = {'BLENDER_RENDER', 'BLENDER_GAME'}
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = 'data'

    @classmethod
    def poll(cls, context):
        '''engine = context.scene.render.engine
        actob = context.active_object
        return (actob and actob.type == 'FONT' and \
                (engine in cls.COMPAT_ENGINES))
        '''
        actob = context.active_object
        return (actob and actob.type == 'FONT')

    def draw(self, context):
        layout = self.layout
        row = layout.row()
        row = row.split(percentage=0.7)
        row.operator_context = 'INVOKE_DEFAULT'
        op = row.operator('text.edit_object_with_tkinter', text='Edit Text Object')
        op = row.operator('text.load_fonts', text='Load Fonts')
        op.setactob = True


# Register
def register():
    bpy.utils.register_class(DATA_PT_call_tkinter)
    bpy.utils.register_class(TEXT_OT_edit_object_with_tkinter)
    bpy.utils.register_class(TEXT_OT_load_fonts)


def unregister():
    bpy.utils.unregister_class(DATA_PT_call_tkinter)
    bpy.utils.unregister_class(TEXT_OT_edit_object_with_tkinter)
    bpy.utils.unregister_class(TEXT_OT_load_fonts)


if __name__ == '__main__':
    register()

いかにしておっぱい画像をダウンロードするか〜2012 をC#で書きました

元ネタ
いかにしておっぱい画像をダウンロードするか〜2012


他にもいくつかの言語で書かれています。
http://d.hatena.ne.jp/D_Rascal/20120320/1332244651


Bing API の制限?があるらしく、ダウンロード枚数が1000枚に届く前に終了してしまいます。

依存ライブラリ

DynamicJson 1.2.0.0
Interactive Extensions 1.1.10621

Program.cs

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Web;
using System.Web.Security;
using Codeplex.Data;

namespace App
{
    class Program
    {
        static void Main()
        {
            const string appid = "";
            const string dir = "./data";
            Directory.CreateDirectory(dir);
            const int num = 50;

            foreach (var response in Enumerable.Range(0, int.MaxValue)
                .Select(_ => (long)_ * 50)
                .Select(page =>
                {
                    var q = HttpUtility.ParseQueryString(string.Empty);
                    q.Add(new NameValueCollection
                        {
                            {"AppId", appid},
                            {"Version", "2.2"},
                            {"Markert", "ja-JP"},
                            {"Sources", "Image"},
                            {"Image.Count", num.ToString(CultureInfo.InvariantCulture)},
                            {"Image.Offset", page.ToString(CultureInfo.InvariantCulture)},
                            {"Adult", "off"},
                            {"Query", "おっぱい"}
                        });
                    return q;
                })
                .Select(q => new Uri("http://api.bing.net/json.aspx?" + HttpUtility.UrlDecode(q.ToString())))
                .Select(WebRequest.CreateDefault)
                .Select(_ => _.GetResponse()))
            {
                string[] urls = EnumerableEx.Using(
                    response.GetResponseStream,
                    stream => (IEnumerable<string>)ToUrls(DynamicJson.Parse(stream)))
                    .ToArray();
                if (!urls.Any())
                {
                    Console.WriteLine("error");
                    return;
                }

                Parallel.ForEach(
                    urls.Select(_ => new
                    {
                        url = _,
                        ext = Path.GetExtension(_),
                        fileName = Path.Combine(dir,
                                                FormsAuthentication.HashPasswordForStoringInConfigFile(
                                                    HttpUtility.UrlEncode(_), "md5") +
                                                Path.GetExtension(_))
                    })
                    .Where(_ => new[] {".jpg", ".jpeg", ".png", ".gif", ".bmp"}.Contains(_.ext)),
                    s =>
                    {
                        Console.WriteLine(s.url);
                        try
                        {
                            using(var wc = new WebClient())
                                wc.DownloadFile(s.url, s.fileName);
                        }
                        catch { }
                    });
            }
        }
        static IEnumerable<string> ToUrls(dynamic source)
        {
            if (source.IsDefined("SearchResponse") &&
                source.SearchResponse.IsDefined("Image") &&
                source.SearchResponse.Image.IsDefined("Results"))
            {
                foreach(var _ in source.SearchResponse.Image.Results)
                {
                    if (!_.IsDefined("MediaUrl")) continue;
                    yield return _.MediaUrl;
                }
            }
        }
    }
}

変化するローカル変数を参照したラムダ式を作成しない

次のメソッドで生成されるラムダ式は、ローカル変数"_"を参照しています。

        static string[] data = { "a", "b", "c", "d" };
        private static IEnumerable<Action> InvokeAction()
        {
            foreach (var _ in data)
            {
                yield return () => Console.WriteLine(_);
            }
        }


これは次の2つのコードで異なる結果を生成します。
※要 Interactive Extensions

        static void Main()
        {
            InvokeAction().ForEach(_ => _());

            InvokeAction().ToList().ForEach(_ => _());
        }
a
b
c
d

d
d
d
d


上のコードの場合、ローカル変数は"a","b","c","d"に初期化され、その都度Console.WriteLineが実行されますが、下のコードの場合はローカル変数が最後に初期化された値でConsole.WriteLineが実行されています。
このような変化するローカル変数を参照したラムダ式を作るとResharperが"Access to modified closure"と知らせてくれます。


どちらの場合でも同じ結果にするためには、ローカル変数を参照せずその値を評価したラムダ式を返すようにすれば良いです。


今回は、Expression TreeとILでDelegateを作成してみます。


次のメソッドで、() => Console.WriteLine(_); を CreateDelegate(_); という呼び出しに変更します。

        private static Action CreateDelegate(string type)
        {
            return Expression.Lambda<Action>(
                Expression.Call(
                    new Action<string>(Console.WriteLine).Method,
                    Expression.Constant(type))).Compile();
        }

        private static Action CreateDelegateIL(string type)
        {
            var dm = new DynamicMethod(string.Empty, typeof (void), new[] {typeof (string)});
            var ilgen = dm.GetILGenerator();
            ilgen.Emit(OpCodes.Ldstr, type);
            ilgen.Emit(OpCodes.Call, new Action<string>(Console.WriteLine).Method);
            ilgen.Emit(OpCodes.Ret);
            return (Action)dm.CreateDelegate(typeof (Action), null);
        }

Expression Treeを使うか、ILを使うかの違いでそれぞれ生成されるDelegateは同じです。

string _ = "a";
CreateDelegate(_);

これで

() => Console.WriteLine("a");

この様なコードを生成します。


これでローカル変数を参照ではなく、ローカル変数の値を評価したうえでのラムダ式を生成することが出来ました。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection.Emit;

namespace ConsoleApplication1
{
    class Program
    {
        static string[] data = { "a", "b", "c", "d" };
        static void Main()
        {
            InvokeAction().ForEach(_ => _());
            Console.WriteLine();

            InvokeAction().ToList().ForEach(_ => _());
            Console.WriteLine();

            InvokeDelegate().ToList().ForEach(_ => _());
            Console.WriteLine();

            InvokeDelegateIL().ToList().ForEach(_ => _());
        }

        private static IEnumerable<Action> InvokeAction()
        {
            foreach (var _ in data)
            {
                yield return () => Console.WriteLine(_);
            }
        }

        private static IEnumerable<Action> InvokeDelegate()
        {
            foreach (var _ in data)
            {
                yield return CreateDelegate(_);
            }
        }

        private static IEnumerable<Action> InvokeDelegateIL()
        {
            foreach (var _ in data)
            {
                yield return CreateDelegateIL(_);
            }
        }

        private static Action CreateDelegate(string type)
        {
            return Expression.Lambda<Action>(
                Expression.Call(
                    new Action<string>(Console.WriteLine).Method,
                    Expression.Constant(type))).Compile();
        }

        private static Action CreateDelegateIL(string type)
        {
            var dm = new DynamicMethod(string.Empty, typeof (void), new[] {typeof (string)});
            var ilgen = dm.GetILGenerator();
            ilgen.Emit(OpCodes.Ldstr, type);
            ilgen.Emit(OpCodes.Call, new Action<string>(Console.WriteLine).Method);
            ilgen.Emit(OpCodes.Ret);
            return (Action)dm.CreateDelegate(typeof (Action), null);
        }
    }
}

GlobalGameJam 2012 札幌会場に参加してきました。


1/27、28、29と3日に渡り開催された48時間でゲームを作るイベントに参加してきました。
僕のいたチームが作った作品はこのページからダウンロードして遊ぶことが出来ます。
http://globalgamejam.org/2012/%E3%81%8E%E3%81%93%E3%81%A1%E3%81%90%E3%82%8F%E3%81%A1%E3%81%8A


27日は夕方からの開始だったのですが会社にGGJへ参加する旨を伝えると休みを取れと言われ休みになりました。
Unityでの開発経験はなく、少しでも足しになればと当日の午前中はUnityの勉強をしていました。
Blenderからアニメーション付きのFBXファイルを吐き出し、モーションの切り替えを試していたのですがうまく行かずに終わってしまいました。今になって思うのはデータのやり取りなどそのあたりを勉強するべきでしたね。


このチームはUnityでの開発だったのですが、2人のプログラマーは触り始めて1週間程度でした。
ですが、もう一人のプログラマーの活躍で形にすることが出来ました。
ほとんどUnityに触れた時間は変わらないはずなのにとても頼りになりました!ありがとうそしてありがとう


僕がプログラムを担当したのは曲の再生だったり、流れてくる譜面だったりそのあたりをやっていました。
Unityでシーケンサーというキーワードで思い出したのがこの記事
http://radiumsoftware.tumblr.com/post/13189647626
この記事を見たときはUnityは使っていないのですが、たまたま思い出しとても参考になりました。


今回このゲームで使用した曲は8小節ごとに別ファイルにしてもらっており、それを指定した順に指定した回数で再生することが出来るようになっています。
時間も差し迫っていたので、最初に用意された順番のまま使用され、その機能が活用されることはありませんでした。


譜面のデータはC#でエディタを作りそれを使用しました。このゲームは何気に2つの難易度が選べるようになっているのですが、このエディタを作っていなければもう一つの難易度の実装は出来なかったと思います。たぶん。そうだと信じたい


それとタイトルなども担当しました。
あのタイトルは仮なのですが、そのアイディアは最終日の昼ご飯の帰りの雑談で決めたものでした。
新たに素材を起こすことなく、かつ蛇の気持ち悪さを前面に出したなんかそんな感じの。
そして2Dの扱いが最後までよくわからず適当です。解像度によってはボタンが画面外に…


知人や会社の人にやってもらった感想も、企画段階に意図したもので大変満足しています。
やりたい要素はもっともっとあったのですが、最低限の部分を入れ込み仕上げられたことにも満足しています。


ゲームと関係のない仕事を1年ほど続けていたのですが、久しぶりにゲーム作りができてとても楽しかったです。
またこういうのやりたいですね。
そして、Unityは大変すばらしいツールです!
C# もすばらしいですね!
BlenderBlender


最後に、今回僕のチームに女の子を入れてくれなかったという仕打ちは忘れません(泣)

ゲームプログラミング勉強会 札幌 1.0に行ってきました。

最近仕事ではゲームの開発から離れてしまっているので刺激を受けるために参加してきました。
Twitterでよく知っている人を何人か拝見できましたし、充実したものになりました。
今話題のUnityやソーシャルゲームの講演もありましたが、個人的には3Dの深い話しが聞きたかったですね。
内容を一部書き忘れていると思います。その点ご了承下さい。

Unity入門(@mswar777さん)

ゲーム制作の大まかなフローやAsset Store、スクリプトがゲーム動かしながらいじれるよ!という話しなどがありました。
弊社でも使用しているようですが、僕は関わっていないので個人的にインストールしてどんなものか試してみようしていました。ですが、何から手を付けれよいのかわからず放置していた、そんな状況でした。


東方の同人ゲーム(幻想テレグノシス)の紹介もされていました。
公式にあるチュートリアルの存在を知れたのは収穫でした。これを利用して学習していくのがいいかもしれません。


これからのゲーム制作の話しは興味深かったです。
Minecraftのようにオンライン上で複数の人が同時に作業しゲームを制作するという話し。おもしろそう。

LuaSquirrelによるゲーム開発入門(@lapis_twさん)

Luaの採用事例やバインドの話しなどがありました。
コンシューマーゲーム開発(に限りませんが)におけるスクリプトは最早なくてはならないものですね。
それでも去年途中で投入されたプロジェクトではスクリプトを使用しておらずかなり驚きました。
僕は自社で開発したスクリプトを使用していてLuaSquirrelも使用しているわけではないので、今後別のプロジェクトではどちらか採用しようと思っています。Squirrelの方がよさげ?

cocos2dによるiPhoneゲーム開発入門

cocos2dというゲームフレームワークの話しでした。
個人的にMacは持っていないので手を上げたのですが、仕事でiPhoneアプリの開発をやっていました。
Objective-Cにある程度慣れてしまえば、このライブラリを使って簡単にゲームが作れてしまうのではないかと錯覚してしまう講演でした。

以下、LT

ソーシャルゲーム(@U_TASさん)(変更後のタイトル覚えてないです)

ソーシャルゲームとは何ぞやという話しなど。
いつかのゲームAIラウンドテーブルの時にソーシャルゲームが題材になったことがあったのですが、そのときに聞いたものとは違いました。
ソーシャルゲームというものはコンシューマゲームと区別するための名前に過ぎず定義があいまいというように言われていましたがそれは違うのではないかと…
区別する必要があるとは思えませんし、お互いいいとこ取りして切磋琢磨すれば良いと思います。

ソーシャル・ブラウザゲームについて(@nazoさん)

この講演はスライドが印象的でした。
ソーシャルゲームが悪く言われる面や、サーバーサイドの技術の話しなど。
僕はソーシャルゲームの儲け方があまり好きではないのですが、サーバー側で最先端技術使いまくりという話しを聞いて印象がかわりましたね。サーバーの技術者としてはソーシャルゲームは面白いものなんだろうなぁと思います。

GlobalGameJam2012開催について(@geekdrumsさん)

LT童貞とは思えない見事な喋りでした。
来年のGGJと福島GGJにあわせて開催されるKawazGameJamのお話しでした。
来年のGGJは是非参加したいな…

Squirrelによる自作クロスプラットフォームゲームエンジンについて(@infosiaさん)

AndroidiOSで動くゲームエンジンemo-frameworkのお話しでした。
かわずたんたたきをこのエンジンで動くように移植したそうです。
Androidでゲームを作る機会があったらちょっと見てみましょう。


ちなみに会場にはお菓子が用意されてました。


全ての講演が終わった後にかわずたんがデザインされたマグカップの抽選会があり、なんと当たってしまいました。

デスクトップの伊波ちゃんかわいい


そんな感じで勉強会は終わりました。
参加された皆さんお疲れ様でした。


懇親会出ればよかったです…

シェーダ(HLSL)でリアルタイムにベクターイメージの描画を行う


開発の環境は次の通りです。
DirectX 9、GeForce 8600 GT、Visual C++ 2010

結論から言うと実用に耐えうるものは作成できませんでした。
これ以上作成を続けても労力に見合ったものが作れる気がしないので中止します。

実用にはなりませんが、なんとなく公開しておきます。


ちなみに、これの作成を思い立ったのは去年の暮れで、
途中まったく作業していませんが5ヶ月ほどかけています。
途中Twitterにはレンダリングしたスクリーンショットをアップしていました。


このベクターイメージの描画で目標としていたのはSVGをシェーダでリアルタイムに描画することでした。
達成できたのは、パスの描画、円(楕円)、長方形を個別に描画することのみで、FPSは4.5程度しか出ておりません。
動作環境やシェーダモデルが3.0より新しいものであれば事情は変わるかもしれません。


では、これらの描画の方法について説明します。
まず、シェーダでのリアルタイムなベクター(ベジェ)の描画というのはすでに存在しています。

http://hosok2.com/project/st/st_coloration.html

この実装と特に異なるのは頂点の座標がシェーダに直接記述されているか、外から与えられるかという点です。


私の考えた方法では、頂点座標は浮動小数点数テクスチャに格納し、シェーダから参照するようにしています。
ベクターの形状を識別するためのパラメータは別の整数のテクスチャに格納し参照しています。
それぞれのテクスチャのフォーマットは、
浮動小数点数テクスチャ:D3DFMT_G32R32F
整数テクスチャ:D3DFMT_A8R8G8B8
です。

テクスチャへのデータの格納を簡単に行うために、IDirect3DTexture9を以下のクラスでラップしています。

template<class T>
class TextureData{
private:
	IDirect3DTexture9* tex;
	D3DLOCKED_RECT lockRect;
public:
	TextureData(IDirect3DTexture9* tex)
		:tex(tex)
	{
		// サーフェイスロック
		tex->LockRect(0, &lockRect, NULL, D3DLOCK_DISCARD);

		D3DSURFACE_DESC d;
		tex->GetLevelDesc(0, &d);
		// テクスチャサーフェイスの初期化
		FillMemory(lockRect.pBits, lockRect.Pitch * d.Height, 0xff);
	}
	~TextureData(){
		// サーフェイスアンロック
		tex->UnlockRect(0);
	}

	inline T& operator[](int index){
		return static_cast<T*>(lockRect.pBits)[index];
	}
	
	void set2(int index, T arg0, T arg1){
		(*this)[ index*2 ] = arg0;
		(*this)[ index*2 + 1] = arg1;
	}
};

パスの描画

パスはベジェ、直線を連結したものです。
ベジェの座標の指定についてはSVGのフォーマットを参考にしています。
http://www.hcn.zaq.ne.jp/___/REC-SVG11-20030114/paths.html#PathDataCubicBezierCommands

ベジェは始点、終点と2つの制御点から成ります。
浮動小数点数テクスチャには始点、制御点1、制御点2、終点の順に頂点座標を格納します。
直線は始点と終点のみなので、始点、終点の順に格納します。

パスの場合、ベジェに続いてベジェや直線が続くため、重複する頂点が現れます。
続くベジェの始点は、直前のベジェ(もしくは直線)の終点なので、それをそのまま利用します。
続くベジェの制御点1は、直前のベジェの制御点2を直前のベジェの終点に対して反対側にとったものになります。
浮動小数点数テクスチャには制御点2、終点を格納します。

直前が直線だった場合、制御点はその直線の終点とします。


パスの始めがベジェの場合、パスの始めが直線の場合、パスの始めでない部分に現れるベジェ(もしくは直線)
この4つのタイプの識別は整数テクスチャに格納します。

また、整数テクスチャには、それぞれの形状で使用する浮動小数点テクスチャのバッファの位置を格納し、
それを用いて座標を取得します。


パスの描画を行う場合、それぞれのテクスチャに以下の様にデータを格納します。

data[0] = 0x00010000; 	//(1)続くデータがパスであることを示す
data[1] = 0xffffffff; 	//(2)線色
data[2] = 0xffff7f00; 	//(3)塗り色
data[3] = 0x1; 			//(4)閉じる

data[4] = 0;	//(5)パスの初めがベジェ

data[5] = 0;	//(6)浮動小数点数テクスチャの位置
data2.set2(0, 0.1f, 0.5f); //(7)浮動小数点数テクスチャの0番目のテクセル
data2.set2(1, 0.1f, 0.9f);
data2.set2(2, 0.4f, 0.9f);
data2.set2(3, 0.5f, 0.5f);

data[6] = 1;	//(8)次はベジェ

data[7] = 4;
data2.set2(4, 0.9f, 0.1f);
data2.set2(5, 0.9f, 0.5f);

data[8] = 3;

data[9] = 6;
data2.set2(6, 0.9f, 0.9f);

data[10] = 0x02000000; //(9)パスの終了

(1)まず、形状を識別するための値を頭に入れます。0x10000はパスのデータが格納されていることを示します。
線の色(2)、塗りの色(3)を指定します。
(4)これはパス始点と終点が結合していない場合の処理で、1であれば始点と終点に直線を引きます。
(5)続く頂点データの意味を指定します。
パスの始めのベジェであれば0、途中に現れるベジェであれば1、パスの始めの直線であれば2、途中に現れる直線であれば3
(6)頂点データの浮動小数点数テクスチャでの位置を指定します。
(7)この場合浮動小数点数テクスチャの0番目のテクセルのRに0.1f、Gに0.5fを格納します。
それぞれシェーダではx,yとして取得することができます。
(8)同様に途中に現れるベジェの指定です。
(9)パスの終了です。


シェーダプログラムでは、まず最初の4つの整数を読み取った後、それぞれの形状によって処理を分けながら
整数を読み進めていきます。パスの場合は0x02000000が現れるまでこの処理を反復しますが、
シェーダプログラムでは無限ループを指定できないため6回のループに制約しています。
0x02000000を読み取る前でも、たくさん接続されたパスは途中で切れてしまうことを意味します。

>パスの描画、円(楕円)、長方形を個別に描画することのみ
冒頭でこのように記述したのも無限ループを作れないため、単独で描画することしかできないためです。
無限ループが作れないのは、おそらく最適化でループを展開しているためだと思うのですが、
6回に制約したループであっても(最適化の所為かわかりませんが)、コンパイルに6分程度の時間がかかってしまいます。

矩形の描画

data[0] = 0x00010001; //矩形
data[1] = 0xffffffff; //線色
data[2] = 0x7f7fff00; //塗り色

data[3] = 0;		//浮動小数点数テクスチャの位置
data2.set2(0, 0.5f, 0.5f); //位置
data2.set2(1, 0.1f, 0.3f); //幅、高さ
data2.set2(2, 3.14f/4.f, 0); //回転(第3引数は未使用)

パスと違いシンプルです。
楕円の描画はdata[0]の0x00010001を0x00010002に置き換えるだけです。
(0x00010000でも0x00010001でもなければ0x00010002でなくてもいいです)



最後にこれらのテクスチャをシェーダに設定します。ほかにもいくつかのパラメータを設定します。

eff->SetTexture( "tex0", texture ); //整数テクスチャ
eff->SetTexture( "ftex", texture2 ); //浮動小数点数テクスチャ
eff->SetInt( "TextureResolution", 16 ); //テクスチャ解像度
eff->SetInt( "g_devide", 18 ); //ベジェの分解能

effはID3DXEffectです。
ベジェは直線に分割して描画することになるので、その分割の数がベジェの分解能です。
ベジェの分解能は18以上の値を入れても18に制限されます。



今回作成したシェーダプログラムです。

float4x4 World;
float4x4 View;
float4x4 Projection;
int TextureResolution;
int g_devide;

texture tex0;
sampler tex_sampler = sampler_state
{
    Texture = <tex0>;
    MinFilter = POINT;
    MagFilter = POINT;
    MipFilter = POINT;
    AddressU = Clamp;
    AddressV = Clamp;
};

texture ftex;
sampler ftex_sampler = sampler_state
{
    Texture = <ftex>;
    MinFilter = POINT;
    MagFilter = POINT;
    MipFilter = POINT;
    AddressU = Clamp;
    AddressV = Clamp;
};
float3x3 transform : TRANSFORM;
float2 screen : SCREEN;

struct VSO
{
	float4 Position : POSITION;
	float2 UV : TEXCOORD0;
	float3 Normal : TEXCOORD1;
	float4 Color : COLOR0;
};
 
VSO mainVS(float4 position : POSITION,
                    float2 tex : TEXCOORD0,
					float4 color : COLOR0,
					float3 normal : TEXCOORD1)
{
    VSO output;
    float4 worldPosition = mul(position, World);
    float4 viewPosition = mul(worldPosition, View);
    output.Position = mul(viewPosition, Projection);
	output.UV = tex;
	output.Color = color;
	output.Normal = normal;
    return output;
}

int toint(float4 f4){
	return (int)(f4.a * 256 * 256 * 256 * 255 +
	f4.r * 256 * 256 * 255 +
	f4.g * 256 * 255 +
	f4.b * 255);
}

float4 bezierColor(float2 UV, float4 lc, float4 fc,
				float2 p0, float2 p1, float2 p2, float2 p3) : COLOR0;
int bezierColorFill(float2 UV, float4 lc, float4 fc,
				float2 p0, float2 p1, float2 p2, float2 p3);
float2 getBezier(float2 p0, float2 p1, float2 p2, float2 p3, float t);
bool isinline(float2 v0, float2 v1, float2 p);
float linepoint_closest(float2 v0, float2 v1, float2 p);
bool islinecross_scan(float2 v0, float2 v1, float y);
float linecross_left(float2 v0, float2 v1, float y);

float4 rectangleColor(float2 UV, float4 lc, float4 fc,
				float2 p0, float2 p1, float2 p2) : COLOR0;
float4 circleColor(float2 UV, float4 lc, float4 fc,
				float2 p0, float2 p1, float2 p2) : COLOR0;

int fetchint(int index){
	return toint(tex2D(tex_sampler, float2((float)(index % TextureResolution)/(float)TextureResolution,
								(float)(index / TextureResolution)/(float)TextureResolution)));
}
float4 fetch(int index){
	return tex2D(tex_sampler, float2((float)(index % TextureResolution)/(float)TextureResolution,
								(float)(index / TextureResolution)/(float)TextureResolution));
}
float2 ffetch(int index){
	return tex2D(ftex_sampler, float2((float)(index % TextureResolution)/(float)TextureResolution,
							(float)(index / TextureResolution)/(float)TextureResolution)).xy;
}

float4 mainPS(VSO input) : COLOR0
{
	int maxloop = 6;
	int tag = fetchint(0);
	// path
	if( tag == 0x00010000L ){
		float4 lc = fetch(1);
		float4 fc = fetch(2);
		int close = fetchint(3);
		int index = 4L;
		float2 startpos = float2(0, 0);
		float2 lastp = float2(0, 0);
		float2 controlp = float2(0, 0);
		int crosscnt = 0;
		while(maxloop-- > 0)
		{
			int s = fetchint(index++);
			int startindex = fetchint(index++);
			float2 p0, p1;
			if(s == 0L || s == 1L){
				if(s == 0L){
					p0 = startpos = ffetch(startindex); startindex++;
					p1 = ffetch(startindex); startindex++;
				}else{
					p0 = lastp;
					p1 = controlp;
				}
				float2 p2 = ffetch(startindex); startindex++;
				float2 p3 = lastp = ffetch(startindex);
				controlp = lastp + -(p2 - p3);
				float4 c = bezierColor(input.UV, lc, fc, p0, p1, p2, p3);
				if( c.a != 0 ) return c;
				crosscnt += bezierColorFill(input.UV, lc, fc, p0, p1, p2, p3);
			}else if(s == 2L || s == 3L){
				if( s == 2L ){
					p0 = startpos = ffetch(startindex); startindex++;
					p1 = lastp = controlp = ffetch(startindex);
				}else{
					p0 = lastp;
					p1 = lastp = controlp = ffetch(startindex);
				}
				
				float len = length( input.UV - p0 );
				if( len <= 0.01 )
					return lc;
				
				len = length( input.UV - p1 );
				if( len <= 0.01 )
					return lc;
					
				if( isinline( p0, p1, input.UV ) ){
					if( linepoint_closest( p0, p1, input.UV ) < 0.01 ){
						return lc;
					}
				}
				
				if( islinecross_scan( p0, p1, input.UV.y ) ){
					if( linecross_left( p0, p1, input.UV.y ) < input.UV.x ){
						crosscnt++;
					}
				}
			}else if(s == 0x02000000L){
				break;
			}
		}
		
		// パスが閉じる指定の場合ここでライン描画されるかチェックする
		if( close != 0 && isinline( startpos, lastp, input.UV ) ){
			if( linepoint_closest( startpos, lastp, input.UV ) < 0.01 ){
				return lc;
			}
		}
		
		// 塗りを行う前にパスを閉じるラインを考慮してカウントする
		if( islinecross_scan( startpos, lastp, input.UV.y ) ){
			if( linecross_left( startpos, lastp, input.UV.y ) < input.UV.x ){
				crosscnt++;
			}
		}
		if( (crosscnt % 2) != 0 ) return fc;
	// rectangle & circle
	}else{
		float4 lc = fetch(1);
		float4 fc = fetch(2);
		int startindex = fetchint(3);
		float2 pos = ffetch(startindex++);
		float2 radius = ffetch(startindex++);
		float2 radian = ffetch(startindex);
		float4 c;
		if( tag == 0x00010001L ){ // rectangle
			c = rectangleColor( input.UV, lc, fc, pos, radius, radian );
		}else{ // circle
			c = circleColor( input.UV, lc, fc, pos, radius, radian );
		}
		if( c.a != 0 ) return c;
	}
	
	return float4(0, 0, 0, 0);
}

float4 bezierColor(float2 UV, float4 lc, float4 fc,
				float2 p0, float2 p1, float2 p2, float2 p3) : COLOR0
{
	int devide = min(g_devide, 18);
	int i;
	// line connect circle
	for(i=0; i<devide + 1; i++){
		float2 p = getBezier(p0, p1, p2, p3, (float)i/(float)devide);
		float len = length( UV - p );
		if( len <= 0.01 )
			return lc;
	}
	// line
	float2 p1_ = getBezier(p0, p1, p2, p3, 0);
	for(i=0; i<devide; i++){
		float2 p0_ = p1_;
		p1_ = getBezier(p0, p1, p2, p3, (float)(i+1)/(float)devide);
		if( isinline( p0_, p1_, UV ) ){
			if( linepoint_closest( p0_, p1_, UV ) < 0.01 ){
				return lc;
			}
		}
	}

	return float4(0, 0, 0, 0);
}


int bezierColorFill(float2 UV, float4 lc, float4 fc,
				float2 p0, float2 p1, float2 p2, float2 p3)
{
	int devide = min(g_devide, 18);
	// fill bezier
	int crosscnt = 0;
	float2 p1_ = getBezier(p0, p1, p2, p3, 0);
	for(int i=0; i<devide; i++){
		float2 p0_ = p1_;
		p1_ = getBezier(p0, p1, p2, p3, (float)(i+1)/(float)devide);
		if( islinecross_scan( p0_, p1_, UV.y ) ){
			if( linecross_left( p0_, p1_, UV.y ) < UV.x ){
				crosscnt++;
			}
		}
	}
	return crosscnt;
}

float2 getBezier(float2 p0, float2 p1, float2 p2, float2 p3, float t){
	float2 v0 = p1 - p0;
	float2 v1 = p2 - p1;
	float2 v2 = p3 - p2;

	float2 p4 = (p0 + v0 * t);
	float2 p5 = (p1 + v1 * t);
	float2 p6 = (p2 + v2 * t);

	float2 v3 = p5 - p4;
	float2 v4 = p6 - p5;
	
	float2 p7 = (p4 + v3 * t);
	float2 p8 = (p5 + v4 * t);

	float2 v5 = p8 - p7;
	
	return p7 + v5 * t;
}

bool isinline(float2 v0, float2 v1, float2 p){
	float2 va = v1 - v0;
	float2 va_ = normalize( va );
	float2 vb = p - v0;

	float d = dot( va_, vb );
	if( d < 0 ) return false;

	return d <= length( va );
}
float linepoint_closest(float2 v0, float2 v1, float2 p){
	float2 va = v1 - v0;
	float2 vb = p - v0;
	return abs( va.x * vb.y - va.y * vb.x ) / length( va );	
}

//スキャンラインと線分がy成分的に交差する可能性があるか
bool islinecross_scan(float2 v0, float2 v1, float y){
	return max( v0.y, v1.y ) >= y && min( v0.y, v1.y ) < y;
}
//直線とスキャンラインの最左端取得
float linecross_left(float2 v0, float2 v1, float y){
	if( v1.y == v0.y ) return max(v0.x, v1.x);
	return (-(v0.x - v1.x) * y - v1.x*v0.y + v0.x*v1.y) / (v1.y - v0.y);
}

float4 rectangleColor(float2 UV, float4 lc, float4 fc,
				float2 p0, float2 p1, float2 p2) : COLOR0
{
	float s = sin(-p2.x);
	float c = cos(-p2.x);
	float2x2 m = { c, s, -s, c };
	UV -= p0;
	UV = mul(UV, m);
	
	UV.x = abs(UV.x) - p1.x;
	UV.y = abs(UV.y) - p1.y;
	
	if( UV.x < - 0.01 && UV.y < - 0.01 ) return fc;		
	if( UV.x < 0.01 && UV.y < 0.01 ) return lc;	
	return float4(0, 0, 0, 0);
}

float4 circleColor(float2 UV, float4 lc, float4 fc,
				float2 p0, float2 p1, float2 p2) : COLOR0
{
	float s = sin(-p2.x);
	float c = cos(-p2.x);
	float2x2 m = { c, s, -s, c };
	UV -= p0;
	UV = mul(UV, m);
	p1 -= float2( 0.01, 0.01 );
	float r = (UV.x * UV.x)/(p1.x * p1.x) + (UV.y * UV.y)/(p1.y * p1.y);
	if( r <= 1 ) return fc;
	p1 += float2( 0.02, 0.02 );
	r = (UV.x * UV.x)/(p1.x * p1.x) + (UV.y * UV.y)/(p1.y * p1.y);
	if( r <= 1 ) return lc;
	return float4(0, 0, 0, 0);
}

technique tec0
{
    pass p0
    {
        VertexShader = compile vs_3_0 mainVS();
        PixelShader   = compile ps_3_0 mainPS();
    }
}

リフレクションを用いてC#とActionScriptで定義されたメソッドを相互に呼び出す

環境はC#(VC# 2008 Express) .NET Framework 3.5 & Flash 8(ActionScript2)
(環境が最新ではないので事情が変わってるかもしれませんが)

ActionScriptで定義されたメソッドの呼び出し

呼び出されるメソッドはあらかじめActionScriptで以下のように記述しておく必要があります。

import flash.external.ExternalInterface;
ExternalInterface.addCallback("hoge", null, hoge);

通常swfに定義されたActionScriptのメソッドは、XMLを作成しFlashのコンテナに渡すことで呼び出しを行います。

C#ではAxShockwaveFlashObjectsのCallFunctionにXMLを渡します。

参考
External API の XML フォーマット

今回は、XMLを暗黙的に作成しメソッドの呼び出しを行えるようにします。
呼び出しのイメージとしては、メソッド呼び出しを仲介するオブジェクトを介し、

FlashPlayer.hoge();

のように呼び出せるようにします。
この場合、FlashPlayer.hoge();の呼び出し時にを作成し、CallFunctionに渡します。

C#で定義されたのメソッド呼び出し

ActionScriptC#に定義されたメソッドを呼び出す場合以下のようにします。

ExternalInterface.call("piyo");

こうするとFlashコンテナのFlashCallイベントがコールされるので、XML(_IShockwaveFlashEvents_FlashCallEventのrequestフィールド)を解析しメソッドを呼び出します。

この場合はリフレクションを用いてメソッドを呼び出すだけです。


上記のそれぞれの機能をまとめたクラス群を以下に示します。

ソース

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using AxShockwaveFlashObjects;
using System.Xml;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Proxies;

namespace FlashObject
{
    /// <summary>
    /// Flashオブジェクトにロードされたswfに定義されたメソッド呼び出しを簡易化するクラス
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public abstract class CallFunc<T> : System.MarshalByRefObject
        where T : new()
    {
        /// <summary>
        /// Ploxyを介したインスタンスを作成します
        /// </summary>
        /// <param name="flashobject"></param>
        /// <returns></returns>
        public static T CreateInstance(AxShockwaveFlash flashobject)
        {
            return (T)(new Ploxy(new T(), flashobject).GetTransparentProxy());
        }

        class Ploxy : RealProxy
        {
            T _this;
            AxShockwaveFlash flashobject;
            public Ploxy(T _this, AxShockwaveFlash flashobject) :
                base(typeof(T))
            {
                this._this = _this;
                this.flashobject = flashobject;
            }
            public override IMessage
                Invoke(IMessage msg)
            {
                var mm = msg as IMethodMessage;
                var ret = invoke(mm.MethodName, mm.Args);
                return new ReturnMessage(ret, null,
                    0, mm.LogicalCallContext, (IMethodCallMessage)msg);
            }

            /// <summary>
            /// Flashオブジェクトにロードされたswfに定義されたメソッドを呼び出す
            /// </summary>
            /// <param name="methodname"></param>
            /// <param name="_params"></param>
            /// <returns></returns>
            private string invoke(string methodname, object[] _params)
            {
                var doc = new XmlDocument();
                var invoke = (XmlElement)doc.AppendChild(doc.CreateElement("invoke"));
                invoke.SetAttribute("name", methodname);
                invoke.SetAttribute("returntype", "xml");
                invoke = (XmlElement)invoke.AppendChild(doc.CreateElement("arguments"));

                foreach (var param in _params)
                {
                    invoke.AppendChild(create_args(doc, param));
                }

                string response = String.Empty;
                try
                {
                    response = flashobject.CallFunction(doc.InnerXml);
                }
                catch
                {
                }

                return response;
            }

            /// <summary>
            /// 引数処理を行う
            /// </summary>
            /// <param name="doc"></param>
            /// <param name="param"></param>
            /// <returns></returns>
            private XmlElement create_args(XmlDocument doc, object param)
            {
                XmlElement element;
                if (param is int
                    || param is uint
                    || param is long
                    || param is ulong
                    || param is short
                    || param is ushort
                    || param is byte
                    )
                {
                    element = doc.CreateElement("number");
                    element.InnerText = param.ToString();
                }
                else if (param is bool)
                {
                    element = doc.CreateElement(param.ToString().ToLower());
                    element.InnerText = "";
                }
                else
                {
                    element = doc.CreateElement("string");
                    element.InnerText = param.ToString();
                }
                return element;
            }
        }
    }

    /// <summary>
    /// Flashオブジェクトからのコールバックを処理するクラス
    /// </summary>
    /// <remarks>
    /// <seealso cref="CallBackProxy&lt;T&gt;">CallBackProxy&lt;T&gt;</seealso>のインスタンス作成を容易にするための
    /// ジェネリックメソッドを提供します
    /// </remarks>
    public class CallBackProxy
    {
        /// <summary>
        /// <seealso cref="CallBackProxy&lt;T&gt;">CallBackProxy&lt;T&gt;</seealso>のインスタンスを作成します
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="owner"></param>
        /// <param name="flashobject"></param>
        /// <returns></returns>
        public static CallBackProxy<T> CreateInstance<T>(T owner, AxShockwaveFlash flashobject)
        {
            return new CallBackProxy<T>(owner, flashobject);
        }
    }

    /// <summary>
    /// Flashオブジェクトからのコールバックを処理するクラス
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class CallBackProxy<T>
    {
        T owner;
        AxShockwaveFlash flashobject;
        /// <summary>
        /// Flashオブジェクトを初期化します
        /// </summary>
        /// <param name="owner"></param>
        /// <param name="flashobject"></param>
        public CallBackProxy(T owner, AxShockwaveFlash flashobject)
        {
            this.owner = owner;
            this.flashobject = flashobject;
            flashobject.FlashCall += FlashCall;
        }

        /// <summary>
        /// Flashオブジェクトからのコールバックイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void FlashCall(object sender, _IShockwaveFlashEvents_FlashCallEvent e)
        {
            var document = new XmlDocument();
            document.LoadXml(e.request);
            callfunc(document);
        }

        /// <summary>
        /// Flashオブジェクトからのコールバック処理
        /// </summary>
        /// <param name="document"></param>
        private void callfunc(XmlDocument document)
        {
            var invoke = document.GetElementsByTagName("invoke");
            string name = invoke[0].Attributes["name"].Value;
            XmlNode _param = document.GetElementsByTagName("arguments")[0];
            var t = owner.GetType();
            var m = t.GetMethod(name);
            m.Invoke(owner, parseparam(_param.ChildNodes));
        }

        /// <summary>
        /// Flashオブジェクトからのコールバック引数処理
        /// </summary>
        /// <param name="_params"></param>
        /// <returns></returns>
        private object[] parseparam(XmlNodeList _params)
        {
            List<object> retparam = new List<object>(_params.Count);
            foreach (XmlNode param in _params)
            {
                if (param.Name == "number")
                {
                    retparam.Add(int.Parse(param.InnerText));
                }
                else if (param.Name == "true"
                    || param.Name == "false")
                {
                    retparam.Add(bool.Parse(param.Name));
                }
                else
                {
                    retparam.Add(param.InnerText);
                }
            }
            return retparam.ToArray();
        }
    }
}

C#からメソッドの呼び出しを行う

メソッドの名前が必要になるので、それらと同じメソッドを定義したクラスを用意します。
ActionScript

function hoge():Void{ }
function hogehoge(arg:Number):Void{ }
function hogehoge2():Number{ }

と定義されていれば、

public class FlashObjectCallFunc : FlashObject.CallFunc<FlashObjectCallFunc>
{
    public void hoge(){ }
    public void hogehoge(int arg){ }
    public int hogehoge2(){ }
}

このようにC#で定義します。

メソッドの呼び出しは以下のようにします。

var axfunc = new FlashObjectCallFunc.CreateInstance(axShockwaveFlash);
axfunc.hoge();

axShockwaveFlashはFlashのコンテナのインスタンスです。

ActionScriptからメソッドの呼び出しを行う

FlashObject.CallBackProxy<App> axcb = FlashObject.CallBackProxy.CreateInstance(owner, owner.axShockwaveFlash);

Appは呼び出されるメソッドのクラスです。
ownerはAppのインスタンスです。

ActionScriptから呼び出す場合以下のようにすればAppクラスのpiyoメソッドを呼び出すことができます。

ExternalInterface.call("piyo");

参考
[サンプル] 透過プロキシ - C# によるプログラミング入門 | ++C++; // 未確認飛行 C