NScripterの深淵

NScripter/NSLua/Lua5.1/関連プログラム

▼メニュー

ゴブリンでもわかるNSLua/Lua5.1 第6回:nil型と真偽値型

前回、新たな2種類の型「真偽値(ブーリアン)型」と「nil型」が出てきた。これまでに登場した「数値・文字列・関数・テーブル」に続く(最後の)主要な型である。

それぞれの型について知っておくべきだろう。

nil

nil(ニル)は形式上型の一種だが、型というよりも「何も代入されていない変数の初期値」という特殊な値に近い。他の言語ではNULL(ヌル)と呼ばれる場合もある。

NScripter本体の変数は初期値が0または""(空文字列)だが、変数の型が決まっていない(動的型付け言語である)Luaにおいては常にnilとなる。いくつかの例を見ながらその性質を掴もう。

--【1】ローカル変数を定義時に何も代入しなかった場合、
--   中身は常にnilとなっている。
local test -->test==nil
local test2=123
local test3="abc"
local test4,test5={}
     -->test4==(空のテーブル)、test5==nil


--【2】関数に渡す引数の数が不足していた場合、
--   不足分の値は全てnilとなる。
function test(a,b,c,d)
    alert(c,d)
end

test("a","b")
-->c,dはnil


--【3】未定義のグローバル変数を参照すると、nilが返る。
alert(aaaaaaaa)-->nil
local hoge=aaaaaaaaaa -->hoge=nil


--【4】テーブル型はnilを「キーが存在しない状態」として扱う。
--   裏を返せばテーブルはキーや値としてnilを持てない。
local tab={["a"]=123,["b"]=456,["c"]=789}

--値が代入されていないキーを参照するとnilが返る
local test=tab["hogehoge"]-->test=nil

--nilを代入するとキーが未定義に戻る
tab["a"]=nil -->キー"a"はテーブルから消える
for k,v in pairs(tab)do
    alert(k,v)-->"b"=456,"c"=789
end

tab[nil]=123 -->エラー!

やや特殊な扱いを受けている型/値であることが見て取れるだろう。【4】の性質は特に重要で、一部言語が抱える「『未定義であるという値』が連想配列内に定義されている」といったような頭おかしいシチュエーションをあらかじめ回避している。

(「引数を受け取る変数」はLuaの内部でローカル変数として定義されるため、【1】と【2】はほとんど同じ内容を説明している。同様に「グローバル変数」はLuaの内部で1つの大きなテーブル(_G)内部に格納されているため、【3】と【4】もほとんど同じ内容を説明している。別に気にしなくてもいいよ。)

真偽値(ブーリアン)型

真偽値(boolean)と呼ばれる型は、true(真)またはfalse(偽)のうちどちらか一方の値を持つ。

--真
local bool=true
--偽
local bool=false

見覚えがないだろうか?

NScripter本体では「"0または1"のみの値を用いてフラグの判定を行う数値変数」、RPGツクールなら「スイッチ(と呼ばれる機能)」が同等の用途で用いられており、「真偽値型」という意識がなくとも機能としては絶対に既知であるはずだ。

on/offを判定するだけであれば別に専用の型を用意する必要など無く、実のところ内部でtrue=1,false=0に変換してしまう言語すらある(Luaは違う)。

では、いかなる理由からわざわざ専用の型が与えられているのか? どこで何に使うのか? 次節以降を参照。

真偽値型とif

前項で確認した通り、真偽値型はしばしばフラグの判定に用いられる。最も頻繁に活躍するのは「真なら条件を満たす、偽なら満たさない」という 1 or 0 のフラグ分岐、すなわちifの条件式内部であろう。

local test=true

--条件式が真ならifの内部が読まれる
if(test==true)then
    --実行される
end

--条件式が偽ならifの内部は読まれない
if(test==false)then
    --実行されない
end

しかし、上記例ではtest==0,test==1で判定するのと何も変わらない。では、以下の例はどうだろうか?

--【1】比較演算子『== ~=(不等号) < > <= >=』は、
--   判定結果として真偽値型を返す
if(1==1)then
-->if(true)then
    --実行される
end

if(1>=0)then
-->if(true)then
    --実行される
end


--【2】and/orは、処理結果として真偽値型を返す
if(1==1 and 2==5)then
-->if(true and false)then
-->if(false)then
    --実行されない
end

真偽値型と「真偽」の定義

ifについて解説した第2回 では、「条件式が判定を満たす場合に節の内部を実行する」と説明した。この説明は間違っていないが言葉足らずであり、正確なifの挙動は「条件式が真である場合に節の内部を実行する」と説明される。

そしてLuaにおいては、falseとnilだけが「偽」、それ以外の値はすべて「真」という大原則(ルール)が定められている。

if(0)then
-->if(true)then
    --実行される
end

local str=""
if(str)then
-->if(true)then
    --実行される
end

local tab={}
if(tab)then
-->if(true)then
    --実行される
end

local h,t=string.find("abc","c",1,1==1)
-->local h,t=string.find("abc","c",1,true)

実のところ、条件式には比較演算子すら必須ではない。

(応用)真偽値型と三項演算

local a,b=false,"abc"
if(a or b)then
-->if(false or "abc")then
-->if("abc")then
-->if(true)then
    --実行される
end

if(a and b)then
-->if(false and "abc")then
-->if(false)then
    --実行されない
end

"A or B"の正確な挙動は「Aが真ならAを、そうでなければBを返す」

"A and B"の正確な挙動は「Aが偽ならAを、真ならBを返す」

となっている。この挙動を利用したテクニックに、「三項演算」と呼ばれる分岐付き代入が存在する。

以下のコードを見てほしい。

local a=123
local b=(a==123 and "真" or "偽")
-->local b=(true and "真" or "偽")
-->local b=("真" or "偽")
-->local b=("真")
-->local b="真"

--「おおよそ」等しい処理
local b
if(a==123)then
    b="真"
else
    b="偽"
end

条件分岐によって代入する値を変えたい場合、三項演算を利用するとifを用いるよりスマートに記述を実現できる。

便利に見えるでしょ? 

「三項演算の多用は可読性を悪化させる」という無視できない問題点が存在する。

local a,b=123,456

--入れ子にすると意味不明になりがち
local c=(a==123 and (b==456 and "真真" or "真偽") or "偽")

--改行を挟めば多少改善する
local c=(a==123 
                 and (b==456 and "真真" or "真偽")
                 or "偽"
        )

Lua開発者公式の解説本にも、「三項演算を使うときはなるべく注釈を書き加えておけ」といった旨の警告が存在する。

三項演算は他にも多少のややこしい問題を伴うが、際限なく話が逸れてゆくためこの辺にしておく。

とりあえず「and/orや比較演算子はif以外でも利用できる」とだけ覚えておいてほしい。

まとめ

やや詰め込み気味になったが、無事だろうか? 

nilは『値なし』を表す変数の初期値である

②フラグたるtrue/falseは条件式の内部処理で頻出する

この二点さえ理解できていればさしあたって大丈夫だろう。

いずれにせよ、今回でLuaの主要な型がすべて揃った。次回以降では、数度に分けて標準ライブラリ(デフォルトで用意されているLua関数群)やNSLua関数群(NScripter側で追加されたもの)をつまみ食いしてゆく。

ゴブリンでもわかるNSLua/Lua5.1 第5回:luasub命令を作ろう

そろそろ座学に飽きたでしょ?

予備知識 関数の「もう一つの」記述方法

関数を作成(・代入)する方法については前々回で学んだ。

hoge=function()
    --ここに中身を書く
     end

local hoge=function()
    --ここに中身を書く
      end

実は関数の代入に限り、以下のような書き方も許されている。

function hoge()
    --ここに中身を書く
end

local function hoge()
    --ここに中身を書く
end

これは一般的なプログラミング言語の文法に見た目を合わせるための特例的ルールであり、両者の解釈は(ほぼ)一緒、すなわち共に「関数の作成」と「指定した名前の変数への代入」となる。

前々回では「関数も変数に放り込む値である(型の一つに過ぎない)」ことを強調する目的で前者の表記を用いていたが、今後は前者を用いる特別な理由が無い限り後者の表記に統一する。こっちの方が見やすいからね。

こうした「見やすくするために特別に許されている別表記」のことをプログラミングの世界ではシンタックスシュガー(糖衣構文)と呼ぶが、名前は別に覚えなくてよい。覚えておくべきは「関数は型の一つに過ぎない」という前提である。

luasub命令を作ってみよう

一つ実践を挟もう。

NScripter本体では実装が難しく、Luaではあっさり作れてしまう命令」のデモンストレーションとして最適な例、

「『渡した文字列』に『特定のキーワード』が含まれるかチェックし、判定結果を返す自作命令」

を作ってゆく。名前はsfindとしよう。

;「$1"あいうえお"が含まれていたら"あいうえお"、
;含まれていなかったら""を$0に代入」
;するNScripter(luasub)命令
sfind $0,$1,"あいうえお"

まずは「NScripterから呼び出す命令(Lua関数)」のガワを作る。

----system.lua-------------
--luasub命令を実行しておく
--命令の名前は"sfind"とする
NSExec("luasub sfind")
--関数を用意(定義)する
function NSCOM_sfind()
    --ここに中身を書く
end

できた。そして中身。「関数」の機能である

①引数を値として受け取る

②受け取った値を使って処理をする

③処理結果を返す

それぞれを順番に作っていこう。

luasub命令:引数を受け取る

Luaにおける通常の関数と異なり、luasub命令がNScripterから引数を受け取るには専用の関数を呼び出さなければならない。

sfind $0,$1,"あいうえお"

このようなluasub命令が呼び出されると、引数である

「$0,$1,"あいうえお"

が(文字列として)内部メモリに一時保存される。

この文字列を先頭から順に解析し、値へ変換するために用意された機能が以下のNSLua関数群である。

--数値を1つ読み込む。変数なら中身を参照する
local num=NSPopInt()
--文字列を1つ読み込む。変数なら中身を参照する
local str=NSPopStr()
--数値変数の番号(%0なら0)を1つ読み込む
local intref=NSPopIntRef()
--文字列変数の番号($1なら1)を1つ読み込む
local strref=NSPopStrRef()

--idを1つ読み込む。結果は文字列として格納される
local id=NSPopId()
--ラベルを1つ読み込む。結果は"*hoge"のような文字列として格納される
local str=NSPopLabel()

--もし直後の文字がカンマなら、
if(NSCheckComma())then
    --カンマ','を読み飛ばす
    NSPopComma()
end

NScripterの命令には、ふつう「引数1,引数2,引数3,引数4,...」 とカンマ区切りの引数が渡されている。つまり、

①受け取るべき値の型に対応した読み込み関数

②カンマを読み飛ばすNSPopComma()

を繰り返し呼び出せばよい。

sfind $0,$1,"あいうえお"

--system.lua------------
function NSCOM_sfind()
    --文字変数の番号を受け取る
    -->【$0】【,$1,"あいうえお"】
    local ref=NSPopStr()

    --カンマを飛ばす
    -->【,】【$1,"あいうえお"】
    NSCheckComma()

    --文字列を受け取る
    -->【$1】【,"あいうえお"】
    local str=NSPopStr()

    --カンマを飛ばす
    -->【,】【"あいうえお"】
    NSCheckComma()

    --文字列を受け取る
    -->【"あいうえお"】【】
    local checkstr=NSPopStr()

    -->ref=0,str="※$1の中身",checkstr="あいうえお"
end

luasub命令:受け取った値を処理する

「文字列Aに文字列Bの値が含まれていれば判定に成功、違ったら失敗」という関数は、都合よくLuaの標準ライブラリに用意されている。

string.find(
    "チェック対象の文字列",
    "キーワード文字列",
    (何文字目を先頭としてチェックするか,)
    (正規表現を使用しないフラグ)
)

今は最も単純な文字列検索を作っているので、判定は先頭1文字目から行い、正規表現は不使用としておく。

local head,tail=string.find(str,checkstr,1,true)

trueという値が急に出てきたが、これについては次回触れる(今まで意図的に紹介していなかった型の一種で、真偽値(ブーリアン)型と呼ばれる。まだ気にしなくていいよ)。

そして正規表現を用いない場合、resultに入る値(すなわちstring.findの戻り値)は以下のようになっている。

①検索に成功した場合、「マッチした文字列=検索に使用した文字列が何文字目〜何文字目に存在するか」を示す2つの数値が返る

②検索に失敗した場合、nilが返る

今度はnilという値が急に出てきた。これも型の一つ…というより「値なし」を示す特殊な値で、真偽値型と同様に次回触れる。

無事判定に成功したなら、返ってきた結果を用いて文字列を切り出しておこう。文字列の切り出しにはstring.sub関数を用いる。

local result
if(head==nil)then
    result=""
else
    --head文字目からtail文字目までstrを切り出す関数
    --(半角換算)
    result=string.sub(str,head,tail)
end

上記コードを統合すると、以下のようになる。

NSExec("luasub sfind")
function NSCOM_sfind()
    --文字変数の番号を受け取る
    local refnum=NSPopStr()
    --カンマを飛ばす
    NSCheckComma()
    --文字列を受け取る
    local str=NSPopStr()
    --カンマを飛ばす
    NSCheckComma()
    --文字列を受け取る
    local checkstr=NSPopStr()

    --判定
    local head,tail=string.find(str,checkstr,1,true)

    --判定に成功していたら文字列を切り出す
    local result
    if(head==nil)then
        result=""
    else
        --head文字目からtail文字目までstrを切り出す関数
        result=string.sub(str,head,tail)
    end

    --以下で判定結果をrefnum番の文字変数に代入したい
    --★★
end

引数を受け取って判定する処理までは仕上がった。あとは結果を返せばよい。

LuaからNScripter変数に値を代入する/変数の中身を覗く

luasub命令における引数読み取りと比べると代入処理は驚くほど単純であり、専用の関数を呼ぶだけでよい。

--NSSetIntValue(num,val)
-- 数値変数num番に数値valを代入する
NSSetIntValue(0,123)

--NSSetStrValue(num,val)
-- 文字列変数num番に文字列valを代入する
-- NScripter本体からは代入困難な文字(「"」など)も代入できる
NSSetStrValue(0,"abc")
NSSetStrValue(0,' " ')

現在作ろうとしていたsfind命令ではrefnum番に文字列resultを代入するのだから、

NSSetStrValue(refnum,result)

目的は果たされた。両関数はluasub命令に限らず、Luaのどこから呼んでも構わない。

反対に、LuaからNScripter変数の中身を取得する関数も提供されている。

--NSGetIntValue(num)
-- 数値変数num番の値を返す
local num=NSGetIntValue(0)

--NSGetStrValue(num)
-- 文字列変数num番の値を返す
local str=NSGetStrValue(0)

まとめ

完成したsfind命令は以下の通り。

---------------------------
--system.lua

NSExec("luasub sfind")
function NSCOM_sfind()
    --文字変数の番号を受け取る
    local refnum=NSPopStr()
    --カンマを飛ばす
    NSCheckComma()
    --文字列を受け取る
    local str=NSPopStr()
    --カンマを飛ばす
    NSCheckComma()
    --文字列を受け取る
    local checkstr=NSPopStr()

    --判定
    local head,tail=string.find(str,checkstr,1,true)

    --判定に成功していたら文字列を切り出す
    local result
    if(head==nil)then
        result=""
    else
        --head文字目からtail文字目までstrを切り出す関数
        result=string.sub(str,head,tail)
    end

    --判定結果をrefnum番の文字変数に代入
    NSSetStrValue(refnum,result)
end

---------------------------------

;00.txt
*define
game
*start

sfind $0,"吾輩は猫である","猫"
;「猫」
「$0」@

sfind $0,"吾輩はダチョウである","猫"
;「」
「$0」@

正規表現文字コード絡みの問題など考慮すべき要素を置き去りにしてはいるものの、とりあえず動作している。

次回は処理の後半に出現した「nil」および「真偽値型」について触れ、Luaの主要な型を掌握してしまおう。

ゴブリンでもわかるNSLua/Lua5.1 第4回:Lua5.1入門 連想配列(テーブル)

プログラミングにおける配列

「複数の変数を一元化して管理できたら楽じゃね?」という需要に応えた概念。

ご存知の通り、通常の変数はしばしば「箱」に喩えられる。値を格納するXという名前の箱!

そして一次元配列は「名札付きのロッカー」に喩えられる。Xという名前のロッカーの、上から3番目の/Aという名前の引き出し!

二次元以上の配列は「住所」に喩えられることが多い。"1"丁目"2"番地"3"号棟の建物!

人類と二次元配列

人間と二次元配列の関わりはかなり古くから存在し、たとえば平安京なんかも二次元配列的([南北][東西])に座標を表していた。

同様に、将棋やチェスの盤面も二次元配列([縦][横])で表される。

中学数学で習った一次関数のグラフ(X,Y)も、その気になれば二次元配列の添え字(キー)として解釈可能である。

もっと身近なところでは、マンションやホテルの部屋番号なども基本的に[階層][部屋番号]の二次元配列で表される。

国内における固定電話の番号も[市外局番][個人の番号]で二次元配列。

意識しようとしまいと、我々人類の日常生活は二次元配列で溢れている。どうだ参ったか。

ゲーム用途では

状態異常のフラグを管理したり、キャラクター単位でステータスを管理したり、イベントクリアフラグの類を一元管理したり、二次元配列で2Dダンジョンのマップデータを管理したり、色んな箇所で用いられている。

NScripterにおける配列

実のところ日頃使っている数値/文字列変数も「"数値/文字列変数"という名前の、長さ4096(変更可能)の一次元配列」として解釈可能である。(もちろん、NScripterユーザーがそのように意識する機会はおそらくほとんど無い)

それとは別にNScripterにも配列変数と呼ばれる概念は存在するものの、制限がとても多い。

*define
;配列変数の宣言
dim ?0[10]
game

*start
;配列変数への代入はmov/movlのみ有効
mov ?0[9],100
;参照だけなら可能だが
add %0,?0[0]
;mov(l)以外の命令で代入が発生するとエラー。不便だね
add ?0[9],1 ;エラー!

;値も添え字も文字列は扱えない。不便だね
mov ?0[9],"あいうえお" ;エラー!
mov ?0["あいうえお"],123 ;エラー!
;こうすることはできる
;numalias number,%0
;mov ?0[number],123
;
;でも、代わりにこの手の記述を用いる方がよほど融通がきく
mov %0,101
mov %%0,10

;なお、セーブデータにNスク配列は含まれない

「mov(l)以外の命令で値を代入できない」という制限が極端に重く、非推奨命令スレスレである。そしてLuaからNスク配列変数へのアクセス手段も無い。なぜか?

Luaにおける配列(テーブル)

Luaにはとても高機能な配列が用意されており、本体配列を丸ごと置き換える手間に足る利便性を有している。

キー(添え字)に文字列等を使える、(関数等と同じく)その場で宣言して良いなど多くの利点を持つ。ひとまずは「だいぶ高機能だなあ」程度の理解で良いのでざっと眺めていこう。

--テーブルを初期化、代入する
local tab={}

--キー(添え字)も値も型の制限は無い。
tab[1]=123
tab["あいうえお"]="かきくけこ"

--当然関数も代入できる。
tab["hoge"]=function(aaa)
        return aaa*3
    end
local num=tab["hoge"](10) -->local num=30
--なんならキーにも使えるが実用性は薄い

--テーブルの値にテーブルを代入してもよい。
--つまり入れ子(多次元)にできる。
tab["hoge"]={}
tab["hoge"]["fuga"]=123

--初期化時に値を代入しても構わない
local tab2={"あ","い","う",{},[5]=100,["キー"]="値",}
-->tab2[1]="あ"
-->tab2[2]="い"
-->tab2[3]="う"
-->tab2[4]={}
-->tab2[5]=100
-->tab2["キー"]="値"

--初期化時に値を並べる場合、
--末尾の「,」はあっても無くてもよい
local tab3={1,2,3,}
local tab4={1,2,3}
--後からコードをいじる際に前者だとミスが減る

local tab5={
    --記述時に改行してもよい
    --というか、
    --基本的にスペースやタブや空行はどこに入れてもよい 
    --(Luaが内部で勝手に整理してくれる)
    --なるべく人間にとって読みやすい書き方を心掛けよう
    --Visual Studio Codeのようなエディタを使うと、
    --ある程度勝手に整形してくれて楽なのでオススメ
    ["hp"]=100,
    ["mp"]=100,
}

--キーの指定に変数を使ってもよい
local tab6={}
local str="abc"
tab6[str]="string" -->tab6["abc"]="string"

Luaの世界では「テーブル」、プログラミング一般でいうと「連想配列」または「辞書配列」と呼ばれる(名前は例によってどうでもいい)。

これまでに登場した「数値型」「文字列型」「関数型」のように、「テーブル型」として概ね等しい扱いになっている。テーブル操作用の関数もいくつか定義されており、それらについては標準ライブラリの紹介時に触れる。

テーブルの参照とコピー

--数値や文字列は代入時にコピーされていた
local str1="あいうえお"
local str2=str1
str2=""
alert(str1)-->"あいうえお"

--テーブルは他の変数に「代入」してもコピーされない
--(同一のテーブルを複数の変数が共有・参照する)
local tab1={1,2,3}
local tab2=tab1
tab2[1]=999
alert(tab1[1])-->999

①最初にテーブルが定義された時点でメモリ上に「本体」が出現する

②以降の代入時、変数は①を参照(アクセス)する

③参照する変数が一個もなくなったテーブルはメモリから自動で消える

こうなっている。巨大な配列を毎回コピーしているとメモリがいくらあっても足りないがゆえ。

(実は関数の代入も同じ仕様だが今は気にしなくてもいい)

「じゃあテーブルをコピーしたい時はどうすんの?」という疑問が湧くかもしれない。

--こうする
local tab1={1,2,3}
loca; tab2={}
for k,v in pairs(tab1) do
    --kにキー(添え字)、vに値が代入される
    alert(k,v)
    tab2[k]=v
end

--「pairs関数」という仕掛けが用意されている。
--内部的には若干ややこしい理屈で動いているので、
--「上記のようにforを書くとキー=値のペアを全部回してくれる」
--とだけ覚えておいてほしい。

頻繁にコピーしたい場合はそういう関数を作ってしまうのが常套手段である。そうした需要が出てくるのは(たぶん)ある程度習熟した後なので、今は忘れてもよい。

Luaの「1オリジン」

そういえば、LuaNScripterと異なり「1」が配列等における数字の基準値(オリジン)となっている。

local tab={}
--ここが先頭
tab[1]=123

--0未満の数値もキーとしては使用できるが、テーブルの「長さ」には含まれない
tab[0]=123
tab[-1]=123 
tab[3.14]=123 

--配列の長さは「#配列名」で取得できる
alert(#tab)
-->alert(1)

--1以降の自然数について、「中身が入っている」キーが
--n個続いていた場合、長さnとなる
tab={1,2,3,4,5} -->#tab==5
tab={[1]=30,[3]=50} -->#tab==1
tab={[10]="",["hoge"]=50} -->#tab==0

テーブルに限った話ではない。NScripterは「0バイト目」から数えるが、Luaは「1文字目」から数える…といった具合に、この基準の差はプログラム全体に及ぶ。

「1オリジン」はプログラミング言語としてはちょっと珍しい(大抵はNScripterと同じく0オリジン)。

慣れれば非常に直感的なのだが、NScripter含む他のプログラミング言語に慣れていると最初は戸惑うかもしれない。

まとめ

テーブルはLuaの中で大きな地位を占めており(実は標準ライブラリの関数も全て巨大なテーブルに入っている)、隅々まで紹介すると初心者向け講座ではなくなってしまう。

「今はまだ覚えなくてよい」だらけになってしまったが、「NScripter本体の配列よりだいぶ高機能っぽい」とさえ分かれば導入としては十分だろう。細部は必要になってから覚えればよい。

次回はNSLua関数(のうちNScripterと値をやり取りするもの)に触れながら、標準ライブラリも紹介してゆく。

--%0の値を取得
local num=NSGetIntValue(0)
--%0に値を代入
NSSetIntValue(0,num)
--$0の値を取得
local str=NSGetStrValue(0)
--$0に値を代入
NSSetStrValue(0,str)

ゴブリンでもわかるNSLua/Lua5.1 第3回:Lua5.1入門 関数を呼び出す/ローカル変数

NScripterにおける「命令」

①引数=値を受け取る

②何らかの処理を実行する

③結果を返す

といった機能を有する。

Luaにおける関数(function)

①値を受け取る

②処理を実行する

③結果=値を返す

といった機能(function)を持つ。

NScripterと同じでしょ?(もちろん本当は色々異なるが今は省略する)

現時点では「とりあえず」命令とは関数である、関数とは命令であると理解してよい。

プログラミング用語としての「関数」

高校数学の初めの方で習ったであろう「f(x)=(x+1)2  y=f(x)」的な記述を思い出せるだろうか。この場合の「関数」は、

「①値を受け取り

 ②受け取った値を用いて何かしら計算し

 ③計算結果を返す

 箱のようなもの」

として説明されなかったか。

プログラミングの世界における「関数」も同じ概念だが、少しだけ柔軟性が高い。値を複数受け取ることもあれば、逆に1つも受け取らないこともある。値を複数返す事もあれば、逆に1つも返さないこともある。

Lua関数の呼び出し方

デフォルトで用意されている関数は色々ある(具体的には次々回あたりで紹介する:それまで我慢してね)が、例として「NSOkBox関数」を呼び出してみよう。

NSOkBox関数はNSLuaによって提供され、NScripterでいうmesbox命令に相当する機能を持つ。つまり、文字列を2つ受け取り、メッセージダイアログを表示する。

--◆「NSOkBox」という名前の変数を、関数として呼び出す

--引数に "タイトル" "本文" 2つの文字列を渡す
    NSOkBox("タイトル","本文")

--引数に aaa bbb 2つの変数の値(=中身)を渡す
    aaa,bbb="タイトル","本文"
    NSOkBox(aaa,bbb)

--引数に何も値を渡さない(エラー)
    NSOkBox()

値を返す関数

関数呼び出しは、代入式の右辺にも記述できる。

--「math.max」関数は2つの数値を受け取り、大きい方の値を返す
    aaa=math.max(10,20)
      -->aaa=20

--[[
math.maxの'.'が気になるかもしれない。
後々説明するので、今はそういう名前だと思っておいてほしい。
--]]

「関数から戻ってくる値/返される値」は、プログラミングにおいて「戻り値」または「返り値」と呼ばれる(return valueの直訳)。

実はNScripterにも同様の概念は存在するが、限定的にしか用いられない…プラグイン呼び出し後のgetret命令! NScripterでは、代わりに変数を直接参照・値を操作することで同様の操作を実現している。

--「%0という引数を受け取り、『%0+10』という値を返し、代入する関数」
add %0,10

関数は入れ子にできる。つまり、関数の引数に関数呼び出しを書いてもよい。

--数学と同じく手前・内側から処理される。
    aaa=math.max(10, math.max(20,30) )
      -->aaa=math.max(10,30)
      -->aaa=30
  
    aaa=math.max(
          --改行や注釈を挟んでもよい。
          10
          ,
          math.max(20,30)
        )

右辺に書ける、入れ子にもできる。おかげで色んな箇所の記述がスマートになるし、一時変数のようなハコも削減できる。

;◆NScripterの場合
;mesbox %0,"%0の中身です" ←本当はこう書きたいがエラーになる
itoa $0,%0
mesbox $0,"%0の中身です"

---------------------------------------------

--◆Luaの場合
--tostring関数は、受け取った値を文字列に変換して返す。
--NSOkBoxに文字列以外を渡すとエラーになるため、
--下記例では数値→文字列と変換している。
NSOkBox(tostring(10),tostring(20))

…言うほどスマートになっているだろうか?

関数を自分で定義する

NSOkBox(tostring(10),tostring(20))

毎回これでは面倒くさいので、以下のような関数があると嬉しい。 「呼び出される際に最大で2つの引数(title,text)を受け取る」 「受け取った引数を文字列に変換し、あらためてNSOkBox()を呼び出す」

alert(10,20)

さっそく作ってみよう。「alertという名前の変数に、関数を代入する」と、alert関数を定義できる。

--[[
◆関数の定義
 変数名=function(受け取る引数名リスト)
             ~処理内容。コードを自由に記述できる~
         end
--]]
alert=function(title,text)
          NSOkBox(tostring(title),tostring(text))
      end

--[[
◆関数の呼び出し
 変数「alert」を関数として呼び出す。
 引数には「10」「20」を渡す
--]]
alert(10,20)

--コード例を簡略化するために、
--ここ以降ではalert関数が定義されているものとする。

「変数に関数を代入する」という言い回しは奇妙に聞こえるかもしれない。

「変数に数値を代入する」 「変数に文字列を代入する」 「変数に関数を代入する」 こう並べてみてほしい。

NScripterの変数は、「文字列型」または「数値型」の2種類のみだった。自作命令の定義には特別な命令であるdefsubを用いる必要があり、定義可能な場所も定義節に限られる。

Luaの変数は、値の一種として「関数」も扱う。数値や文字列と同じく自由に関数を定義・代入できるし、どこからでも上書きできる。

--定義
test=function(str)
         return str.."だよ"
     end
alert( test("テスト") ) -->"テストだよ"

--別の変数に代入
test2=test
alert( test2("実験") ) -->"実験だよ"

--上書き
test=function(str)
         return str.."ですよ"
     end
alert( test("テスト") ) -->"テストですよ"

色々な関数定義の例

(処理内容自体に実用性は無い)

--◆戻り値を返す関数
test=function(num)
        --数値を受け取り、2倍した値を返す
        return num*2
     end
a=test(100) -->a=200


--◆関数内部にあれこれコードを書く
test=function(num)
        --数値を受け取り、正なら2倍、負なら-10倍する関数
        if(num>0)then
            return num*2
        else
            return num*-10
        end
     end
a=test(100) -->a=200
a=test(-10) -->a=100


--◆複数の引数、複数の戻り値
test2=function(num,num2)
        --2つの数値を受け取り、2つの数値を返す関数
        return num+1,num2+2
      end
a,b=test2(100,200) -->a,b=101,202

ところで:ローカル変数

自作関数で引数を受け取るために使った変数名(ここまでの例ではtitle,text,num,num2など)は、「関数内部でのみ利用可能な変数」となる。

test=function(aaa)
         ----
     end
alert(aaa)-->aaaは未定義

こうした変数(一部のコードからのみ見えている変数)をローカル変数と呼ぶ。下記例の通り、たとえ関数の外部で同名の変数が定義されていたとしても、ローカル変数が定義されていた場合はそちらが優先される。

aaa=3
alert(aaa)-->3

test=function(aaa)
        alert(aaa)
     end
test(10)-->10

実はforの変数もローカル変数である。

for i=1,10 do
    alert(i)-->i=1,2,3,4,5,...,10
end
alert(i)-->iは未定義

ローカル変数はそれ以外の場所でも定義できる。

if(1==1)then
    --ifの中でのみ有効
    local aaa    
    aaa=10
end

if(1==1)then
    --入れ子になっている場合、
    --コード位置より上の階層にあるローカル変数のみ有効
    local aaa
    aaa=10
    if(1==1)then
        local aaa    
        aaa=20
        alert(aaa)-->20
    end
    alert(aaa)-->10
end
alert(aaa)-->aaaは未定義


for i=1,10 do
    --forの中でのみ有効、初期値は100
    local bbb=100
end


test=function()
        --この関数内部でのみ有効、初期値は"あいう","えお"
        local ccc,ddd="あいう","えお"
     end

test=function(num)
        --ローカル変数に関数を代入してもよい
        local fff=function(aaa)
                    return aaa*2
              end
        
        return fff(num)
        
     end

こうした「ローカル変数の有効範囲」の事をブロック/スコープと読んだりするが、名前は忘れていい。

    --「do」は「if(1==1)then」等と等しい
    --(ブロックをなし、判定なしに内部を実行する)
    do
        local aaa=1
    end

    if(1==1)then
        local aaa=1
    end

ローカル変数の何が便利か。

*define
numalias temp1,%0 :inc %0
numalias temp2,%0 :inc %0
numalias temp3,%0 :inc %0
numalias temp4,%0 :inc %0

        ......

;defsub命令または汎用サブルーチン
*mygosub
    mov %temp1,...
    mov %temp2,...
return

*mygosub2
    mov %temp1,...
    mov %temp2,...
return    

あなたが一定程度面倒臭がりで、しかも手元にあるNScripterスクリプトが一定以上複雑だったなら、きっとこの手の汎用変数が用意されているはずだ。

スクリプトの特定箇所でしか使わない変数名をいちいち定義節に戻って書くのってめんどくね?」と思った事はないだろうか。

gosub先から多重にgosubする際、使う汎用変数がうっかり被ったりしないか気を回した事などもあるかもしれない。

ローカル変数を使えばそうしたアレコレが一切必要なくなる。その場でしか使わない変数はその場で定義する!

ここまででNScripter命令に相当する関数の呼び出しと定義、便利なローカル変数について知った事になる。それで関数にどんなもんが用意されてるのさ?と気になる気持ちは理解しつつ、次回は少し寄り道をして超便利な配列…連想配列について説明する。

local tab={}
tab[1]=2
tab["hoge"]=4

余談:

どこからでも参照・利用できる変数(今まで使っていたヤツら)は、Luaにおいて「グローバル変数」と呼ばれる。NScripterにおける通常変数は、このLuaの定義を当てはめた場合「グローバル変数」であることになる。名前被って紛らわしいね。

「その場で使いきりの変数」には基本的にローカル変数が用いられると認識してよい。だからこれまで書いてきたグローバル変数だらけのコードはほとんど嘘っぱちなんです。

ゴブリンでもわかるNSLua/Lua5.1 第2回:Lua5.1入門 変数の四則演算

前二稿で、「Lua自体のチュートリアルをこなせばNScripterLuaを扱える」状態になった。本稿ではLuaチュートリアルに進む。

(……実のところ、Lua5.1それ自体のチュートリアルは公式のガイドブックが一番親切で分かりやすい。都道府県単位なら一冊くらいは在庫があるはずなので、紙の本に抵抗がない人はもうこのページ閉じて地元の図書館に取り寄せ予約を出す方がよい。)

それはそれとして、まずは「NScripterの延長で理解できる変数定義/四則演算」から。

変数の宣言、代入、四則演算

~以下、system.lua内に記述されている前提~

--「--」で始まる行は注釈・コメントアウト
--NScripterの「;」に相当する

--[[
    複数行をまとめてコメントアウトできる
    便利。
    --[=[
        入れ子にもできる
        とても便利。
    --]=]
--]]

--変数は好きな場所でいきなり定義してよい(代入が定義・宣言を兼ねる)
aaa=1
--変数名の大文字・小文字は区別される。大雑把だった人は注意
AAA=2

--複数の変数をまとめて定義・代入してよい
bbb,ccc=1,2

--命令間の区切りはスペースさえ空いていればよい
--(Luaが自動で解釈してくれる)
bbb=1 ccc=2
--もし明示したければ「;」で区切ってもよい
bbb=1;ccc=2;

--四則演算+-*/の扱いはNScripterと変わらない
aaa=(1+2-3*4)/5

--ただしmod(剰余、割った余り)は「%」で表す
aaa=10 % 3 -->1

--累乗は「^」で表す 3^2なら3の2乗
aaa=3^2

--根号(√)は0.5乗で表せる
aaa=3^0.5

--右辺に変数を書いてもよい
aaa=aaa+1

--文字列は 「""」または「''」で括る
ddd="12345"  --コメントは行末にも書ける

--文字列の結合は「..」で行う。+ではないので注意
eee="あいう".."えお"

--数字もそのまま結合できる
fff="123"..45

--ただし、数字の直後に「..」を書く際はスペースを空ける必要がある
--めんどいね
ggg=123 .. "45"
--小数点と結合を明確に区別するための仕様である
--Luaにおける「数値」は常に小数点以下(浮動小数点)を扱う
hhh=3.14
ggg=3/2 -->ggg=1.5

上記例ではLuaならではの便利機能が(複数行コメントアウト程度しか)見えず、NScripter側とやり取りする手段も含まれない。これだけでは何も始まらないが、とりあえず「基本的な言語仕様はNScripterと比べても別に複雑ではない」とさえ理解できれば十分だろう。

ifとfor

ifは複数行にまたがって書ける点、そして「elseif」「else」の存在が大きな相違箇所。NScripterでifとjumpfなりを延々組み合わせていたような判定はスマートに書けるようになる。

forは「指定回数ループするだけなら」NScripterとほぼ一緒。Luaならでのループは他にあるが、ここでは見てすぐ分かる回数ループのみ紹介する。

a="123"
b=456
c="789"

--if(判定)then
--    判定を満たす場合に実行する処理
--    複数行にまたがって書いてよい
--end

--「==」と書く必要がある。「=」はダメ
  if(a=="123")then
    --aは"123"である
  end
  if(b<=999)then
    --bは999以下である
  end


--「and」は&&に、「or」は||に相当する
  if(a=="123" and b==456)then
    --判定を満たした
  end
  if(a=="123" or b==456)then
    --判定を満たした
  end

--ややこしい判定は複数行にまたがって書いてもよい
  if(
    --内部に注釈も書ける。
    (a=="123" and b==456)
    or
    c=="789"
  )then
    --満たした
  end


--if(判定)then
--    満たす場合に実行する処理
--else
--    満たさない場合に実行する処理
--end
  if(a=="123")then
    --aは"123"である
  else
    --aは"123"ではない
  end


--if(判定1)then
--    判定1を満たす場合に実行する処理
--elseif(判定2)then --※判定1を満たさない場合のみ追加で判定
--    判定2を満たす場合に実行する処理
--else
--    全ての条件式を満たさなかった場合に実行する処理
--end
  if(a=="999")then
    --aは"999"である
  elseif(a=="123")then
    --aは"123"である
  else
    --aは"999"でも"123"でもない
  end


for i=1,10 do
    --10回繰り返す
    --「ループ回数は初回のみ計算する」という相違点もあるにはあるが、
    --NScripterユーザーは元々この仕様を意識していないので忘れてよい
end

for n=10,1,-1 do
    --見た目はNScripterとほとんど変わらない(A TO B STEP C)
end

(少なくとも基礎的な部分だけなら)NScripterとそこまで大きくは変わらない、おそるべき言語ではないと分かったかと思う。

次項ではNScripterでいう「命令」に相当する骨子、「関数」を紹介する。

--ダイアログが出現する
aaa="テスト"
NSOkBox(aaa,"テストです")

ゴブリンでもわかるNSLua/Lua5.1 第1回:もうちょっと導入

NScripterからLuaが呼び出されるタイミング

大別して3種類ある。

Lua言語それ自体についてのチュートリアル以前に、「いつ呼び出される/呼び出せるのか」という前提を確認しておく。

①「system.luaに記述されたLuaスクリプトが、定義節終了時に自動で読み込み・実行される」

----------00.txt------------------- 
*define 
game ;ここでsystem.luaが読み込まれる 
*start ;実行節の処理
 -----------system.lua--------------- 
--ここで何かを処理。通常はluasub命令の定義など

「system.luaの自動読み込み/実行完了までは定義節」となる。

NScripterから呼び出すためのLua関数を定義する

・system.luaからさらに別のluaファイル/プラグインを呼び出す

大抵はこうした処理を行う。

②「Luaに記述された関数を、NScripter命令として呼び出す」

----------00.txt-------------------
*define
game
*start
testtest ;NSCOM_testtest()関数が呼び出される

-----------system.lua---------------
--■testtest命令を実行すると呼び出される
NSExec("luasub testtest")
function NSCOM_testtest()
    --ここで何かを処理
end

NScripter本体におけるdefsub(新規命令定義)と同じ役割。「呼び出す先がNScripterラベルではなく、Lua関数である」点だけが異なる。本体スクリプトと異なるファイルに書かなきゃいけないのは面倒に思えるかもしれないが、基本的にLuaという言語の利便性が手間を上回るので安心してよい。

③「Luaに記述された関数が、NScripterの特定の動作に反応して自動で呼び出される(コールバック)」

----------00.txt-------------------
*define
game
*start
savegame 1 ;セーブ実行直前にNSCALL_save()関数が呼び出される

-----------system.lua---------------
--■セーブ直前に自動で呼び出される
NSExec("luacall save")
function NSCALL_save()
    --ここで何かを処理
end

システムカスタマイズと同じ動作だが、システムカスタマイズより割り込める動作の種類が多い。個々のコールバック解説は[ここに書いた]ので、Luaチュートリアルを終えてからあらためて覗くとよい。

これでNScripterからLuaを呼び出す道筋がついた。次回からLuaチュートリアル

ゴブリンでもわかるNSLua/Lua5.1 第0回:物事の前提と導入

「NSLuaはわからん」なNScripterユーザーを全員Lua扱える状態に持っていくための記事。ただし、Lua言語自体の初心者向け解説は次ページ以降に書く。

Luaとは?

プログラミング言語の一種。NScripterに組み込まれたのはLua5.1。Lua5.1は最も人気だった時期のバージョンであり、その辺のLua解説記事も大抵は同バージョンを前提にしている。NScripter公式の同梱物に、Luaという言語それ自体に関する解説は付属していない。

NSLuaとは?

プログラミング言語ではないNScripterからLuaを呼び出す仕掛け、およびLuaからNScripterの情報を読み書き可能にするために追加された関数(命令)群を指す。

NScripter本体側の命令で実装しにくい処理(文字列周りの操作や扱いやすい配列など)をLuaは標準ライブラリ(命令)として有するため、ややこしいスクリプトの組みやすさも天と地ほど変わる。誇張ではない。

一方で、様々な不幸……

「『NSLua』とは『Lua言語』を参照するための一連の仕掛けである」という関係性がやや曖昧に説明されていたこと。

公式同梱物にはLua言語自体のチュートリアルチュートリアルへの導線が含まれていなかったこと。

解説記事を書いてくれた人々がプログラミング分かっている勢だったこと。

等々、少なからぬNScripterユーザーにLuaという言語が食わず嫌いされてしまうに至る歴史的経緯もあった。Luaにおける変数定義とか四則演算とか、おそらくはそのレベルからチュートリアルが求められていたのだろう。

それはそれとして

NScripterでNSLuaを扱うには、Luaというプログラミング言語を習得すればよい」と整理される。大丈夫、NScripter使えるならLuaも絶対すぐ使えるようになる(超軽量な言語なので)。

次回以降で最低限のチュートリアルを行うが、とりあえず本稿ではNScripterからLuaを呼び出せる環境を準備する。

NScripterでLuaを使う準備
nscr.exe
00.txt
system.lua 自分で新規作成する
[dllフォルダ]
 ┣nslua.dll 公式が同梱している
 ┗nspng.dll 公式が同梱している

実はファイル置くだけで準備完了する。

system.luaは拡張子を変えただけのプレーンテキストファイルで、この中に書かれたLuaスクリプトが起点となる。編集時はメモ帳なりと関連付けて開けばよい。

※なお2021年現在、Windows10メモ帳標準の文字コードがShift-Jisではなくなった。00.txt/system.luaともに新規保存時は注意が必要。