[開発ログ]Tk3d実行速度の違い(Windows)
同じWindowsマシンでもcygwinとmswin32で速度が異なる。 tkのバージョンのせいだろうか?
mswin32
>ruby -v ruby 1.8.1 (2003-12-25) [i386-mswin32] ruby -rtk -e 'p Tk::TCL_VERSION' "8.3" loop time(1.344ms)
cygwin
$ ruby -v ruby 1.8.1 (2003-12-25) [i386-cygwin] $ ruby -rtk -e 'p Tk::TK_VERSION' "8.4" loop time(1.648ms)
tk3d.rbを更新
require("tk") module Tk3d # poss : [[x1,y1,z1],[x2,y2,z2],[x3,y3,z3],...] # angle : [ax,ay,az] def Tk3d.rotate_zyx(poss,angle) ax = angle[0] ay = angle[1] az = angle[2] poss.collect!{|px,py,pz| # Z軸回転 xx = px * Math.cos(az) - py * Math.sin(az) yy = px * Math.sin(az) + py * Math.cos(az) px = xx py = yy # Y軸回転 zz = pz * Math.cos(ay) - px * Math.sin(ay) xx = pz * Math.sin(ay) + px * Math.cos(ay) pz = zz px = xx # X軸回転 yy = py * Math.cos(ax) - pz * Math.sin(ax) zz = py * Math.sin(ax) + pz * Math.cos(ax) py = yy pz = zz [px,py,pz] } end def Tk3d.rotate_xyz(poss,angle) ax = angle[0] ay = angle[1] az = angle[2] poss.collect!{|px,py,pz| # X軸回転 yy = py * Math.cos(ax) - pz * Math.sin(ax) zz = py * Math.sin(ax) + pz * Math.cos(ax) py = yy pz = zz # Y軸回転 zz = pz * Math.cos(ay) - px * Math.sin(ay) xx = pz * Math.sin(ay) + px * Math.cos(ay) pz = zz px = xx # Z軸回転 xx = px * Math.cos(az) - py * Math.sin(az) yy = px * Math.sin(az) + py * Math.cos(az) px = xx py = yy [px,py,pz] } end def Tk3d.transfer(poss,position) mx = position[0] my = position[1] mz = position[2] poss.collect!{|pos| [pos[0]+mx,pos[1]+my,pos[2]+mz] } end def Tk3d.to_screen(poss,cx,cy,screen_z,scale_x=1.0,scale_y=1.0) npos = [] poss.each{|px,py,pz| npos << [scale_x*px*screen_z/pz + cx, scale_y*py*screen_z/pz + cy] } npos end def Tk3d.av_z(poss) av = 0 poss.each{|px,py,pz| av += pz } av /= poss.size end def Tk3d.min_z(poss) min = poss[0][2] poss.each{|px,py,pz| min = pz if pz<min } min end end class Tk3dCanvas < TkCanvas attr_accessor :scale attr_accessor :camera attr_accessor :scale def initialize(parent=nil, keys=nil) super @scale = 1.0 end def quit @canvas.destroy end end class Tk3dCamera attr_accessor :ppos attr_accessor :angle attr_accessor :length attr_accessor :position attr_accessor :screen_z def initialize(screen_z=1000.0,ppos=nil,ang=nil) @screen_z = screen_z @ppos = ppos ? ppos : [0.0,0.0,0.0] @angle = ang ? ang : [0.0,0.0,0.0] end def flush npos=[[0,0,-@length]] Tk3d::rotate_xyz(npos,@angle.collect{|v|v*=-1}) npos.flatten! @position = [@ppos[0]+npos[0], @ppos[1]+npos[1], @ppos[2]+npos[2]] flag = (@position_last != @position || @angle_last != @angle || @length_last != @length) @position_last = @position.dup @angle_last = @angle.dup @length_last = @length flag end end class Tk3dOt attr_reader :z_rate attr_reader :tag_to_z def initialize(canvas3d,z_rate=1.0) @canvas3d = canvas3d @z_rate = z_rate @unused = {} @used = [] @tag_to_z = {} @prims = {} # 描画の基準となるウィジェット。このウィジェットの描画順序がこのOtの描画順序となる @base_prim = TkcPolygon.new(@canvas3d, []) end def insert_unused(z,data) @unused[z] = [] unless @unused[z] @unused[z] << data end def clear_unused @unused.clear end def unused_empty? @unused.empty? end def add_prim(key,value) @prims[key] = value end def get_prim(key) @prims[key] end def get_prim_all @prims end def z_to_neartag(z) unless a = @used[z..-1] return nil end return a.compact.flatten[0] end def draw(max) c = @canvas3d n = 0 @unused.each{|z,v| while !v.empty? (tag,color,pos2d) = v.shift c.coords(tag, pos2d) # @canvas3d.get_prim(tag).fill("#000000") if old_z = @tag_to_z[tag] if @used[old_z].size <= 1 @used[old_z] = nil else @used[old_z].delete(tag) end end if ltag = z_to_neartag(z) prim = @prims[ltag] else prim = @base_prim end @prims[tag].raise(prim) @tag_to_z[tag] = z @used[z] = [] unless @used[z] @used[z] << tag return if (n += 1) > max end @unused.delete(z) } end end # Tk3dModel # * モデリングデータ(構成要素)を保持 # * ワールド内での位置と角度を保持 # * OTに描画TAGを登録 class Tk3dModel attr_accessor :position attr_accessor :angle attr_reader :tag_to_data def initialize(canvas3d, ot, data) @canvas3d = canvas3d @ot = ot @tag_to_data = {} @data = data @data.collect!{|lcolor,color,pos| prim = TkcPolygon.new(@canvas3d, []) do outline(lcolor) fill(color) end tag = prim.id.to_s prim.addtag(tag) @ot.add_prim(tag,prim) @tag_to_data[tag] = [color,pos] [tag,color,pos] } @position = [0.0,0.0,0.0] @angle = [0.0,0.0,0.0] end def add_ot camera_angle = @canvas3d.camera.angle camera_position = @canvas3d.camera.position.collect{|v|v*=-1} screen_z = @canvas3d.camera.screen_z cx = TkWinfo.width(@canvas3d)/2 -2 cy = TkWinfo.height(@canvas3d)/2 -2 scale_x = @canvas3d.scale * cx / (@canvas3d.width/2) scale_y = @canvas3d.scale * cy / (@canvas3d.height/2) near_z = 0 z_rate = @ot.z_rate @data.each{|tag,color,pos| npos = pos.dup #--ローカル変換 Tk3d::rotate_zyx(npos, @angle) Tk3d::transfer(npos, @position) #--ワールド変換 Tk3d::transfer(npos, camera_position) Tk3d::rotate_zyx(npos, camera_angle) #--スクリーン投影 if Tk3d::min_z(npos) > near_z#--nearクリップ pos2d = Tk3d::to_screen(npos, cx, cy, screen_z, scale_x, scale_y) z = (Tk3d::av_z(npos)*z_rate).to_i else pos2d = [0,0,0,0] z = (near_z*z_rate).to_i end @ot.insert_unused(z,[tag,color,pos2d]) } end end if $0 == __FILE__ TkRoot.new.bind("KeyPress-Escape", proc{ @after.stop;TkRoot.new.destroy }) @camera = Tk3dCamera.new(1000) @camera.length = 500 @canvas3d = Tk3dCanvas.new do width(200) height(160) background("#888888") pack('fill' => 'both', 'expand' => true) end @canvas3d.camera = @camera @canvas3d.scale = 0.2 @ot_bg = Tk3dOt.new(@canvas3d,0) @ot = Tk3dOt.new(@canvas3d,8.0) # Model @model = [] 8.times{|i| w = -15.0 h = -40.0 @model << m = Tk3dModel.new(@canvas3d,@ot, [ ["","#777777",[[0.0,0.0,0.0],[-w,h,-w],[-w,h, w]]], ["","#888888",[[0.0,0.0,0.0],[-w,h, w],[ w,h, w]]], ["","#888888",[[0.0,0.0,0.0],[ w,h, w],[ w,h,-w]]], ["","#777777",[[0.0,0.0,0.0],[ w,h,-w],[-w,h,-w]]], ["","#999999",[[-w,h,-w],[-w,h,w],[w,h,w],[w,h,-w]]], ] ) x = i%2==0 ? i/2*40 : -(i+1)/2*40 m.position[0] = x } # BG Model n = 8 w = 64 data = [] n.times{|z| n.times{|x| h = 0.0 x1 = x4 = x*w - n*w/2 x2 = x3 = (x+0.8)*w - n*w/2 z1 = z2 = z*w - n*w/2 z3 = z4 = (z+0.8)*w - n*w/2 c = 0xAA - ((x1*x1+z1*z1)/(n*w*4)).to_i.abs c = 0x00 if c < 0x00 color = sprintf("#%02x%02x%02x",c,c,c) data << ["",color,[[x1,h,z1],[x2,h,z2],[x3,h,z3],[x4,h,z4]]] } } @bg_model = Tk3dModel.new(@canvas3d,@ot_bg,data) @camera.angle = [0,0,0] @camera.ppos = [0,-20,0] @camera.angle[0] = Math::PI/180.0*10 @camera.angle[1] = Math::PI/180.0*80 draw_max = n*n TkScale.new { from(1) to(n*n) set(n*n) orient 'horizontal' command { |val| draw_max = val } pack } #-------------------- counter = 0 @ct = 0 @t = Time.now @after = TkAfter.new(10, -1, proc { if counter%2 == 0 @camera.angle[1] += -Math::PI/180.0*2 end if @camera.flush @ot_bg.clear_unused @bg_model.add_ot end if @ot.unused_empty? ct = 0 @model.each{|m| ct += 1 next if @ct%1 != ct%1 m.angle[1] += Math::PI/180.0*(rand(4)+8) m.add_ot } @ct += 1 end @ot.draw(50) @ot_bg.draw(draw_max) counter += 1 if counter%20 == 0 puts " loop time(#{Time.now-@t}ms)" @t = Time.now end }) @after.cancel_on_exception = false @after.start #-------------------- Tk.mainloop end
[アイデア][開発ログ]Nadokaで遊ぶ
Nadokaボディ(model-A) NDK_BodyAクラス
ユーザの目に見えるNadokaをクライアントのワールドウィンドウの中に実体化する。 Nadokaヘルムを装着することでIRCDに接続し、アイテムを用いてbot的な役割を果たす。 現在モデルAのデザインが進行中。モデルBやモデルCが存在するかは不明。
Nadokaヘルム NDK_Helmクラス
このヘルメットにIRCクライアントサーバプログラムNadokaが組み込まれている。 実体化したNadokaボディはこれを装着しなければ一切のIRC処理を行えない。 実はこれこそ本体である。ボディはワールドでの単なる虚像にすぎないのだ。
[開発ログ]Tk3dとNadokaを組み込む
Tk3dとNadokaをクライアント本体に組み込んだ。 クライアントのチャットウィンドウと既存のIRCクライアント間でのチャットに成功。
total 9683 lines .cgi 1274 .rb 5881 .sql 411 .txt 266 .htm 636 .html 1215
[Ruby][開発ログ]tk3d.rbプロトタイプ
- Zクリップ (実装)
- ポリゴン単位のZソート (実装)
- 平行光源の光源計算 (未実装)
少し変わった作りをしている。部分的な描画更新が可能。具体的に言うとオーダリングテーブル単位で更新のレートを変更できる。この機能を実現するために表示済みのポリゴンの中からZ値を元に描画するポリゴンのエントリ位置を割り出す処理(z_to_neartag(z)メソッド)が入っているが、この部分が重い。サンプル実行時に下部にあるスケールバーを下げるとBGの更新間隔が変化するのがわかる。
この機能をつけた理由は、BGを初めに描画して視点変更が発生するまでキャラクターオブジェクトだけを更新する、という仕組みを実現するため。
Ruby/Tk実行環境をお持ちの方はスコアをご報告頂けるとありがたい。
手元の環境でのスコア(バーは初期値の64)
loop time(1.359ms) P4 2.8Gz WindowsXP
tk3d.rb
require("tk") module Tk3d # poss : [[x1,y1,z1],[x2,y2,z2],[x3,y3,z3],...] # angle : [ax,ay,az] def Tk3d.rotate_zyx(poss,angle) ax = angle[0] ay = angle[1] az = angle[2] poss.collect!{|px,py,pz| # Z軸回転 xx = px * Math.cos(az) - py * Math.sin(az) yy = px * Math.sin(az) + py * Math.cos(az) px = xx py = yy # Y軸回転 zz = pz * Math.cos(ay) - px * Math.sin(ay) xx = pz * Math.sin(ay) + px * Math.cos(ay) pz = zz px = xx # X軸回転 yy = py * Math.cos(ax) - pz * Math.sin(ax) zz = py * Math.sin(ax) + pz * Math.cos(ax) py = yy pz = zz [px,py,pz] } end def Tk3d.rotate_xyz(poss,angle) ax = angle[0] ay = angle[1] az = angle[2] poss.collect!{|px,py,pz| # X軸回転 yy = py * Math.cos(ax) - pz * Math.sin(ax) zz = py * Math.sin(ax) + pz * Math.cos(ax) py = yy pz = zz # Y軸回転 zz = pz * Math.cos(ay) - px * Math.sin(ay) xx = pz * Math.sin(ay) + px * Math.cos(ay) pz = zz px = xx # Z軸回転 xx = px * Math.cos(az) - py * Math.sin(az) yy = px * Math.sin(az) + py * Math.cos(az) px = xx py = yy [px,py,pz] } end def Tk3d.transfer(poss,position) mx = position[0] my = position[1] mz = position[2] poss.collect!{|pos| [pos[0]+mx,pos[1]+my,pos[2]+mz] } end def Tk3d.to_screen(poss,cx,cy,screen_z,scale_x=1.0,scale_y=1.0) npos = [] poss.each{|px,py,pz| npos << [scale_x*px*screen_z/pz + cx, scale_y*py*screen_z/pz + cy] } npos end def Tk3d.av_z(poss) av = 0 poss.each{|px,py,pz| av += pz } av /= poss.size end def Tk3d.min_z(poss) min = poss[0][2] poss.each{|px,py,pz| min = pz if pz<min } min end end class Tk3dOt attr_reader :z_rate def initialize(world,z_rate=1.0) @world = world @z_rate = z_rate @unused = {} @used = [] @tag_to_z = {} @prims = {} # 描画の基準となるウィジェット。このウィジェットの描画順序がこのOtの描画順序となる @base_prim = TkcPolygon.new(@world.canvas, []) end def insert_unused(z,data) @unused[z] = [] unless @unused[z] @unused[z] << data end def add_prim(key,value) @prims[key] = value end def get_prim(key) @prims[key] end def empty? @unused.empty? end def z_to_neartag(z) unless a = @used[z..-1] return nil end return a.compact.flatten[0] end def draw(max) c = @world.canvas n = 0 @unused.each{|z,v| while !v.empty? (tag,color,pos2d) = v.shift c.coords(tag, pos2d) # @world.get_prim(tag).fill("#000000") if old_z = @tag_to_z[tag] if @used[old_z].size <= 1 @used[old_z] = nil else @used[old_z].delete(tag) end end if ltag = z_to_neartag(z) prim = @prims[ltag] else prim = @base_prim end @prims[tag].raise(prim) @tag_to_z[tag] = z @used[z] = [] unless @used[z] @used[z] << tag return if (n += 1) >= max end @unused.delete(z) } end end class Tk3dCamera attr_reader :ppos attr_reader :angle attr_accessor :position attr_accessor :screen_z def initialize(screen_z=1000.0,ppos=nil,ang=nil) @screen_z = screen_z @ppos = ppos ? ppos : [0.0,0.0,0.0] @angle = ang ? ang : [0.0,0.0,0.0] end def length=(l) @length = l flush_position end def angle=(a) @angle = a flush_position end def ppos=(a) @ppos = a flush_position end def flush_position npos=[[0,0,-@length]] Tk3d::rotate_xyz(npos,@angle.collect{|v|v*=-1}) npos.flatten! @position = [@ppos[0]+npos[0], @ppos[1]+npos[1], @ppos[2]+npos[2]] end end class Tk3dWorld attr_accessor :scale def initialize(camera, cwidth, cheight, color) @camera = camera @canvas = TkCanvas.new do width(cwidth) height(cheight) background(color) pack('fill' => 'both', 'expand' => true) end @scale = 1.0 end def quit @canvas.destroy end def canvas @canvas end def camera @camera end end # Tk3dModel # * モデリングデータ(構成要素)を保持 # * ワールド内での位置と角度を保持 # * OTに描画TAGを登録 class Tk3dModel attr_accessor :position attr_accessor :angle def initialize(world,ot, data) @world = world @ot = ot @data = data @data.collect!{|color,pos| prim = TkcPolygon.new(@world.canvas, []) do outline("#666666") fill(color) end tag = prim.id.to_s prim.addtag(tag) @ot.add_prim(tag,prim) [tag,color,pos] } @position = [0.0,0.0,0.0] @angle = [0.0,0.0,0.0] end def draw ct = 0 camera_angle = @world.camera.angle camera_position = @world.camera.position.collect{|v|v*=-1} screen_z = @world.camera.screen_z cx = TkWinfo.width(@world.canvas)/2 -2 cy = TkWinfo.height(@world.canvas)/2 -2 scale_x = @world.scale * cx / (@world.canvas.width/2) scale_y = @world.scale * cy / (@world.canvas.height/2) near_z = 0 z_rate = @ot.z_rate @data.each{|tag,color,pos| npos = pos.dup #--ローカル変換 Tk3d::rotate_zyx(npos, @angle) Tk3d::transfer(npos, @position) #--ワールド変換 Tk3d::transfer(npos, camera_position) Tk3d::rotate_zyx(npos, camera_angle) #--スクリーン投影 if Tk3d::min_z(npos) > near_z#--nearクリップ pos2d = Tk3d::to_screen(npos, cx, cy, screen_z, scale_x, scale_y) z = (Tk3d::av_z(npos)*z_rate).to_i else pos2d = [0,0,0,0] z = (near_z*z_rate).to_i end @ot.insert_unused(z,[tag,color,pos2d]) ct += 1 } end end if $0 == __FILE__ TkRoot.new.geometry("+100+100") TkRoot.new.bind("KeyPress-Escape", proc{ @after.stop;TkRoot.new.destroy }) @camera = Tk3dCamera.new(1000) @camera.length = 500 @world = Tk3dWorld.new(@camera,200,160,"#bbbbbb") @world.scale = 0.2 @ot_bg = Tk3dOt.new(@world,0) @ot = Tk3dOt.new(@world,8.0) # Model @model = [] 8.times{|i| w = -15.0 h = -40.0 @model << m = Tk3dModel.new(@world,@ot, [ ["#777777",[[0.0,0.0,0.0],[-w,h,-w],[-w,h, w]]], ["#888888",[[0.0,0.0,0.0],[-w,h, w],[ w,h, w]]], ["#888888",[[0.0,0.0,0.0],[ w,h, w],[ w,h,-w]]], ["#777777",[[0.0,0.0,0.0],[ w,h,-w],[-w,h,-w]]], ["#999999",[[-w,h,-w],[-w,h,w],[w,h,w],[w,h,-w]]], ] ) x = i%2==0 ? i/2*40 : -(i+1)/2*40 m.position[0] = x } # BG Model n = 8 w = 64 data = [] n.times{|z| n.times{|x| h = 0.0 x1 = x4 = x*w - n*w/2 x2 = x3 = (x+0.8)*w - n*w/2 z1 = z2 = z*w - n*w/2 z3 = z4 = (z+0.8)*w - n*w/2 c = 0xAA - ((x1*x1+z1*z1)/(n*w*4)).to_i.abs c = 0x00 if c < 0x00 color = sprintf("#%02x%02x%02x",c,c,c) data << [color,[[x1,h,z1],[x2,h,z2],[x3,h,z3],[x4,h,z4]]] } } @bg_model = Tk3dModel.new(@world,@ot_bg,data) @camera.angle = [0,0,0] @camera.ppos = [0,-20,0] @camera.angle[0] = Math::PI/180.0*10 @camera.angle[1] = Math::PI/180.0*80 @camera.flush_position flush_num = n*n TkScale.new { from(1) to(n*n) set(n*n) orient 'horizontal' command { |val| flush_num = val } pack } #-------------------- counter = 0 @ct = 0 @t = Time.now @after = TkAfter.new(10, -1, proc { @camera.angle[1] += -Math::PI/180.0*1 @camera.flush_position if @ot_bg.empty? @bg_model.draw end if @ot.empty? ct = 0 @model.each{|m| ct += 1 next if @ct%1 != ct%1 m.draw m.angle[1] += Math::PI/180.0*(rand(4)+8) } @ct += 1 end @ot.draw(50) @ot_bg.draw(flush_num) counter += 1 if counter%20 == 0 puts " loop time(#{Time.now-@t}ms)" @t = Time.now end }) @after.cancel_on_exception = false @after.start #-------------------- Tk.mainloop end
[Ruby]Perlを使ってみる
新しい掲示板を設置するのにPerlを久しぶりに使う。既存のスクリプトだから基本的にはただ設置すればいいのだが、微妙にカスタマイズしたくてPerlスクリプトを改造する。Perlといえば。いまでこそ完全なRuby使いと化してしまったオレも、一年半前まではPerl使いだった。Perl暦は5年以上か。しかし、しかしだ。さっぱりPerlが書けない。頭がRubyモードになっているせいもあるが、それを差し引いてもPerlは使いにくい。うん、やっぱりRubyがいいと再認識する。
掲示板の改造は結局半分しか出来なかったのだが、いまさらPerlを勉強する気にはなれないし、放置するか。
[開発ログ]ワールドクライアントの制作開始
まずは簡単なものをつくって公開しよう。
ワールドクライアントver0.1仕様
- ユーザサイドから見た仕様
- Windowsで動作するexeファイル形式の配布
- Rubyがあれば動作するrbスクリプトとしても配布
- チャットが可能
- CGIで登録したアカウントでログイン出来る
- 3Dマップが表示される
- PCが表示される
- エリアが複数存在する
- PCは3Dマップ上を移動できる
- (本来PCは霊魂。実体化にはエイリアスが必要)
- システムサイドから見た仕様
- 認証、エリア移動はCGI
- チャット、マップ移動はIRC
- グラフィックシステムはRuby/Tkで実装
よし、Tkからはじめよう。
[アイデア]ユーザがNPCや町を作成
一般的なNPCの簡単な機能は例えばこうだ。
- 話しかけると決まった台詞を話す
- あるフラグによって話す台詞が変化する。変化の条件としては
- ○○○のアイテムを持っている
- お金を○○○以上持っている
- レベルが○○以上
- ○○○フラグが立っている
- 台詞の表示と同時になんらかの処理を行う。例えば
- PCが持つアイテム○○○とNPCが持つアイテム△△△を交換
- PCが持つお金とNPCが持つアイテム○○○を交換(買う)
- PCが持つアイテム○○○とNPCが持つお金を交換(売る)
- あるフラグを立てる
- あるフラグを降ろす
- NPCがPCに○○○の魔法を使う
- NPCがPCに○○○のアイテムを使う
単純だがこれだけの機能があれば一応NPCを演じることが出来るし、 それなりのイベントを構築することも可能だ。 お使い系のクエストならこれだけで十分だ。
と、ここまでは普通のゲームなら当たり前に実装されているシステムだが、 これを一歩進めたシステム考える。 ユーザが持つキャラクタ(PC)にこの機能が自由に使えるように設定できるようにする。 これは開発するのもユーザが設定するのもそれほど難しくはない。 ユーザのパラメータ設定で作る簡単なボットだ。 このボットは台詞や上記の処理を自由に設定しておけば、 あとは他のプレイヤーが話しかけたときに、設定した通りの振る舞いをする。 そしてそのボットを複数おけるようにすれば新しい町だって作れるだろう。
これは単にアイテムを売るだけのバザーキャラより面白そうだ。