[開発ログ]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)にこの機能が自由に使えるように設定できるようにする。 これは開発するのもユーザが設定するのもそれほど難しくはない。 ユーザのパラメータ設定で作る簡単なボットだ。 このボットは台詞や上記の処理を自由に設定しておけば、 あとは他のプレイヤーが話しかけたときに、設定した通りの振る舞いをする。 そしてそのボットを複数おけるようにすれば新しい町だって作れるだろう。
これは単にアイテムを売るだけのバザーキャラより面白そうだ。