「YAYAプログラミングテクニック〜アンカーで用語解説を実装する」

この記事では、「YAYA」で作成されたゴーストのトークに「アンカー」を付加し、用語の解説へリンクする方法について説明します。

具体的なコーディングについては以下のファイルを参考にしてください。


(この記事は、「伺かアドベントカレンダー2010」に向けて書かれたものです)

まえがき

近年人気のある伺かゴーストのジャンルとして、いわゆる「専門トークゴースト」、あるいは「世界観ゴースト」というものがあります。専門トークゴーストは軍事や音楽などの(しばしば実在する)マニアックな知識を披露するゴースト、一方で世界観ゴーストはゴースト作者が創作した独自の世界観に基づいたゴーストで、どちらかといえば正反対のジャンルといえます。

しかし、2つのジャンルに共通する性質もあります。たとえば、どちらのジャンルのゴーストも難解な用語が出てきやすく、しかもそれらが登場人物にとって当たり前の用語である場合が多いため、キャラクタのトークの中で用語の解説が十分になされず、ユーザがトークの意味を理解できないという弊害がしばしば起こります。

この「難解用語問題」に対処するための現実的な方法として、伺かゴーストでしばしば使われているのが、「アンカー」と呼ばれる手法です。トーク本文中の単語にリンクが埋め込まれ、リンク先に解説を設置するというもので、すでに様々なゴーストで採用されています。



  
「音符」という文字をクリックすると解説トーク(右)に移行する


というのも、主要なSHIORIの一つである里々では既にアンカーを実装する仕組みが用意されているため、里々ゴーストではしばしば利用されているのですが、残念ながらYAYA*1ではそういう仕組みがないために、自分でコードを組まなくてはいけません。


この記事では、YAYAでアンカーを使った用語解説を実装するためには何が必要かを、簡単に解説していきます。

アンカーを実装するために必要な3つのこと

アンカーは、トーク本文中に埋め込まれた特定の単語をクリックすることで、別の解説文を表示する機能です。アンカー単語がトークの中に登場すると色が変わり下線が付けられます(装飾は変えることもできます)。
アンカーを付加するためには、トーク本文の中にアンカーを明示するタグ(\_a[])を挿入する必要があります。
ユーザがアンカーをクリックすると、OnAnchorSelect イベントが発生します。OnAnchorSelect イベントの Reference0 にはどのアンカーがクリックされたかの情報(identifier、識別子)が渡され、それを利用して該当する解説文を表示する、という流れになります。つまり、アンカーの実装に必要なのは、

  • アンカー単語の定義
  • トークにアンカーを付加する関数
  • アンカーがクリックされた時に解説を表示する関数(イベント)

の3つとなります。

さくらスクリプトにおけるアンカーの書式

ここでアンカーの書式をおさらいしておきましょう。

この部分に\_a[identifier]アンカー\_aが付きます

前項でも説明したように、さくらスクリプト内でこのように書くと、「アンカー」という単語にアンカーが付きます。「アンカー」をクリックするとOnAnchorSelect イベントが発生し、Reference0 に identifier (識別子)が渡されます。

書式からもわかるように、さくらスクリプトでアンカーを定義するのに最低限必要なのは、「アンカー(リンク)となる単語」「リンク先の解説文章」、そして identifier の3つです。

アンカーとなる用語の保持

さまざまな用語に解説をつけるためには、そのデータをどこかに保存しなくてはいけません。ここでは、YAYAの配列機能を利用して、用語を保持することにします。*2


さて、ここで、次のようなフォーマットで単語を定義することにします。

'<単語>|<読み>|<特殊制御>|<解説本文>'

実際の記述例

'音符|おんぷ||\u\s[10]\h\s[20]音符(おんぷ)は、\w5音楽を定量的に書く上で、(後略)'


<単語>と<解説本文>は説明がいらないと思います。ここではさらに、将来利用するために<読み>と<特殊制御>という2つのエントリも追加していますが、気にしなくて構いません。*3

この単語定義を、YAYAのarray記法を使って汎用配列にしておきます。(NOBさんの記事も見てね!)

ANCHOR.GetAnchorWords : array
{
  '<単語1>|<読み1>|<特殊制御1>|<解説本文1>'
  '<単語2>|<読み2>|<特殊制御2>|<解説本文2>'
}

ところで、この定義には identifier が見あたりません。何故なら、実装上 identifier は(どの単語であるかを判別できるなら)何でもよく、たとえば <単語> をそのまま利用してもいいからです。「ユーリ・クラシカル 2」では、identifier として配列のインデックス(配列の何番目かを表す数字)を利用しています。

本文にアンカーをつける…その前に

さて、用語も定義できたことですし、次はトーク本文にアンカーをつけてみましょう。
前にも書いた通り、アンカーの書式は単純です。単語は配列で定義してあるので、FOR構文を使って、本文中に現れる「解説が定義された単語」をすべて置換してやればOKです。

ANCHOR.AddAnchorAlways
{
	// アンカーを付加したいトーク(引数で与える)
	_script = _argv[0]
	
	// アンカー定義単語数
	_asize = ARRAYSIZE( AnchorWords )
	_i = 0
	
	for _i=0; _i

あとは、このアンカーをクリックした時の動作を定義します。

OnAnchorSelect
{
	// 引数は単語のインデックス
	_index = TOINT(Reference0)
	
	// アンカー単語定義配列から、該当する単語を引っ張ってくる
	_array = SPLIT(AnchorWords[TOINT(_index)],'|')
	
	_answer = _array[3] // 解説文
	_answer = ANCHOR.AddAnchorAlways( _answer ) // 解説文にアンカーを付加
	_answer // 解説文を返す
}

これで、アンカーの実装にに必要な3要素、

  • アンカー単語の定義
  • トークにアンカーを付加する関数
  • アンカーがクリックされた時に解説を表示する関数(イベント)

がすべて実装できました。おわり。ぱちぱちぱち。


――となれば良かったのですが、残念ながらこのコードは思った通りには動きません。いくつかの問題を解決する必要があります。

置換に伴う問題点

ここで問題です。

  '音楽|おんがく||音を楽しむ学問のこと。'
  '現代音楽|げんだいおんがく||二十世紀以降につくられた音楽のこと。'

この2単語が定義されているとき、

'次の演奏会で、現代音楽をやることになったよ。'

というトークにアンカーをつけます。きちんと動作するでしょうか?

このトークにアンカーをつける場合、

'次の演奏会で、\_a[1]現代音楽\_aをやることになったよ。'

となるのが望ましいでしょう。(「1」は単語「現代音楽」のインデックス。YAYAの配列は0ベースのため、単語「音楽」のインデックスは「0」)

しかし、実際には、

'次の演奏会で、現代\_[0]音楽\_aをやることになったよ。'

となってしまうはずです。何故でしょうか?

そうです。「音楽」が、「現代音楽」よりも“前”に定義されているため、本当なら長い「現代音楽」にアンカーを付けたかったのに、より短い「音楽」にアンカーがついてしまったのでした。

先に挙げた通り、アンカーを付加する関数は定義された単語を単純なループで見て行くため、先に定義された単語が先に置換されてしまうのです。


では、「現代音楽」を先に定義してやればいいでしょうか?

  '現代音楽|げんだいおんがく||二十世紀以降につくられた音楽のこと。'
  '音楽|おんがく||音を楽しむ学問のこと。'

はたして、実行結果はというと、

'次の演奏会で、\_a[0]現代\_a[1]音楽\_a\_aをやることになったよ。'

もっとひどいことになってしまいました。もうおわかりだと思いますが、「現代音楽」に「音楽」が内包されているため、後から「音楽」を置換したときにも反応してしまっています。\_aタグは多重に定義することができないので、このスクリプトは正常に動作しません。


つまり、正常に動作させるには、次の2つを実現する必要があります。

  • 単語長(文字数)が大きい順に置換する。
  • 一度置換された文字は、再度置換されないようにする。

どのようにすればいいでしょうか?

一度置換された文字の処理について

2番目の、「一度置換された文字は、再度置換されないようにする」のは、比較的簡単で、置換を2回に分けてやればOKです。
「現代音楽」にアンカーを付加するとき、

'次の演奏会で、\_a[0]現代音楽\_aをやることになったよ。'

のように直接単語を出力せず、

'次の演奏会で、\_a[0]<<これは0番の単語だよ!>>\_aをやることになったよ。'

というような中間出力とします。
この状態で次の「音楽」を置換しても、「現代音楽」という単語が存在しないので、余計なアンカーは付加されません。
すべての単語について置換が終わった段階で、<<これは0番の単語だよ!>>という文字列を実際の「現代音楽」に再置換してやれば、

'次の演奏会で、\_a[0]現代音楽\_aをやることになったよ。'

という望みの出力を得ることができます。

単語の長さによるソート

もう一つの「単語長(文字数)が大きい順に置換する」は少々骨が折れそうです。
アンカー単語を、文字数が多い順に定義してやれば済む話ですが、うっかり定義する位置を間違えると誤動作するというのもうすら寒い話です。せっかく配列として定義しているのですから、定義する順番が前後すると動作が変わってしまうような実装にはしたくありません。

じゃあどうすれば良いのかというと、アンカー単語を文字数の多い順に並べ替える関数があれば良いわけです。
少々長いですが、こんな感じの関数になります。

//////////////////////////////////////////////////////////////////
//
//  指定番目の項目の文字列長をもとに並べ替える関数
//  クイックソート 降順
//
//////////////////////////////////////////////////////////////////

// USAGE: SortByLength( _a as String, i as Integer, j as Integer, Delimiter as String, 
//                      TargetItemNumber as Integer )
// ソートされる行列は _a で指定された名前の行列

ANCHOR.SortByLength
{
	_a = _argv[0]
	
	_first = TOINT(_argv[1])
	_last  = TOINT(_argv[2])
	_d = _argv[3]
	_n = TOINT(_argv[4])
	
	// 中央の要素の単語の長さを閾値とする
	_x = STRLEN( EVAL(" %(EVAL('_a'))[(_first + _last)/2][ _n, _d ] " ) )
	_i = _first
	_j = _last

	while 1
	{
	
		while ( STRLEN( EVAL(" %(EVAL('_a'))[_i][ _n, _d ] ") ) > _x )
		{
			_i++
		}	
		while ( _x > STRLEN( EVAL(" %(EVAL('_a'))[_j][ _n, _d ] ") ) )
		{
			_j--
		}
			
		if ( _i >= _j )
		{
			break
		}	
		
		EVAL("_t = %(EVAL('_a'))[_i] ")
		EVAL(" %(EVAL('_a'))[_i] = %(EVAL('_a'))[_j] ")
		EVAL(" %(EVAL('_a'))[_j] = _t ")
		_i++
		_j--
		
	}
	
	if ( _first < _i-1 )
	{
		ANCHOR.SortByLength( _a, _first, _i-1, _d, _n )
	}	
	if ( _j+1 < _last  )
	{
		ANCHOR.SortByLength( _a, _j+1, _last , _d, _n )
	}	
	
}

これはクイックソートと呼ばれるアルゴリズムで、知られているソートアルゴリズムの中ではもっとも高速に動作するものの一つです。今回この記事では詳しい解説をしませんが、与えられた配列(ここではもちろんアンカー単語を定義した配列)を文字数の多い順に並べ変えてしまいます。

まとめ、さらにその先へ

アンカーによる用語解説を実装するためには、

  • アンカー単語の定義
  • トークにアンカーを付加する関数
  • アンカーがクリックされた時に解説を表示する関数(イベント)

の3要素が必要です。さらに、

  • 単語長(文字数)が大きい順に置換する
  • 一度置換された文字は、再度置換されないようにする

という2つの条件をクリアする必要がありました。前項までで、この5項目をすべて実現したので、最低限の用語解説を実装できました。

ここでわざわざ「最低限の」と付け加えたのは、これだけでは非常に使い勝手が悪いからです。たとえば単語をクリックして解説を表示したら、元のトークに戻ることができません。一つのトークに2つ以上のアンカーがある場合、1回につき1単語しか解説を読むことができません。この場合、解説文の最後に元のトークへ戻るリンクがあれば便利ですよね。


他にも、今回の記事では紙面の都合で実装まで踏み込みませんが、たとえばこんな機能があるとさらに便利になります:

  • アンカーのつく単語の一覧(索引)表示機能
  • 用語解説の検索機能
  • 単語のリダイレクト、表示非表示など
    • 違う表記だけど同じ意味の単語を一つにまとめる方法
  • 用語解説以外にもアンカーを使う
    • 上の実装だと、アンカーがほぼ用語解説専用になってしまっているので、identifier に接頭辞をつけるなどして名前空間を分割する

記事の先頭で紹介した「ユーリ・クラシカル 2」の辞書にはこれらがすべて実装されていますので、興味のある方は参考にしてみてください。
(検索機能は disabled になっていますので、動作は「レチハルカ」でご確認ください)



  
(左)索引機能とリダイレクトの例(ユーリ・クラシカル 2) (右)検索結果表示の例(レチハルカ)


Wikipedia などの百科事典巡りにハマるような人にとって、用語解説アンカーは宝の山のようなものです。
ゴーストのトークの理解を深めるために利用するもよし、読み物としてコンテンツの一部とするもよし。
用語解説、あなたも今日から試してみませんか?

補遺

  • 2010/12/27 小節追加、微修正



*1:改造版では実装されているかもしれませんが…

*2:YAYAの配列の便利な利用法については、12月13日付のNOBさんの記事も参照してください

*3:索引を作るときには、あいうえお順に並べ替えるために<読み>エントリが必要になります。