ユニットに性別を加えるMODの作成を通して、主にXML,DLL,Python間の連携について学びます。
Civilization Bts 3.13 日本語版を使用。
DLLのソースコードも同封
ユニットの属性に性別を導入します。前人未到の18禁MOD開発への足掛かりであり妄想は広がりますが、初期段階としてまずは極力簡素な仕様を決めましょう。
これを作ります。
MOD名はSexにします。My Documents\My Games\Beyond the Sword(J)\MODS\Sex フォルダを作成。
ユニットが性別をもつかどうかの情報をXMLに加えます。ユニットの種類ごとの情報は C:\Program Files\CYBERFRONT\Sid Meier's Civilization 4(J)\Beyond the Sword(J)\Assets\XML\Units\CIV4UnitInfos.xml に書かれていることはご存知でしょう。このXMLにおもむろに要素を加える、とその前に同じフォルダにある CIV4UnitSchema.xml を編集する必要があります。Schema(スキーマ)とはXMLの構造のことです。どの要素にどの要素が含まれているのか、例えば、UnitInfosタグにはUnitInfoが複数含まれていて……、という情報がこのファイルに書かれています。では MODS\Sex\Assets\XML\Units に CIV4UnitSchema.xml をコピーして、エディタで開いてください。"UnitInfo" で検索すると以下のような行が見つかります。
... 前略 ... <ElementType name="UnitInfo" content="eltOnly"> <element type="Class"/> <element type="Type"/> <element type="UniqueNames"/> <element type="Special"/> ... 中略 ... <element type="iLeaderExperience"/> <element type="iOrderPriority" minOccurs="0"/> </ElementType> ... 中略 ... <ElementType name="Class" content="textOnly"/> <ElementType name="UniqueName" content="textOnly"/> <ElementType name="UniqueNames" content="eltOnly"> <element type="UniqueName" minOccurs="0" maxOccurs="*"/> </ElementType> <ElementType name="Special" content="textOnly"/> ... 後略 ...これが表しているものはなんとなくわかりますね。UnitInfo定義の末尾に要素bHasSexを加えます。
... <element type="iLeaderExperience"/> <element type="iOrderPriority" minOccurs="0"/> <!-- Sex MOD begin --> <element type="bHasSex"/> <!-- Sex MOD end --> </ElementType> <!-- Sex MOD begin --> <ElementType name="bHasSex" content="textOnly" dt:type="boolean"/> <!-- Sex MOD end --> ...
Civ4のコードの変数名にはハンガリアン記法が使われているので従っておきましょう。今回は性別を持つかどうかの真偽値なので boolean の b を頭に付けています。
続いて MODS\Sex\Assets\XML\Units に CIV4UnitInfos.xml をコピーして編集。各 UnitInfo に bHasSex 要素を加えます。
... <Civ4UnitInfos xmlns="x-schema:CIV4UnitSchema.xml"> <UnitInfos> <UnitInfo> <Class>UNITCLASS_LION</Class> <Type>UNIT_LION</Type> ... <LeaderPromotion>NONE</LeaderPromotion> <iLeaderExperience>0</iLeaderExperience> <bHasSex>1</bHasSex> </UnitInfo> <UnitInfo> <Class>UNITCLASS_BEAR</Class> <Type>UNIT_BEAR</Type> ...
ここでゲームを起動してみましょう。ゲームには何の変化もありませんが、XMLの文法エラーがないことが確認できます。
ユニットオブジェクトに性別属性を追加するためにDLLのC++コードを編集します。DLL作成環境のセットアップはMOD/作成情報/CvGameCoreDLL.dllの作り方を参照。今の段階では元のコードのコピペレベルの事しかしないので、XML編集に比べて格別難解ということはありません。
ロードしたCIV4UnitInfos.xmlのbHasSexの値をC++のコードから参照できるようにします。ユニット情報はCvInfo.h/.cppファイル中のCvUnitInfoに保存されるのでこれをいじります。他のXMLの変数がやっていることを真似すればOKです。Read/Writeの順番を一致させるよう気をつけましょう
public: DllExport bool hasSex() const; // Exposed to Python protected: bool m_bHasSex;
CvUnitInfo::CvUnitInfo() : m_iAIWeight(0), ... m_bHasSex(false) { } ... bool CvUnitInfo::hasSex() const { return m_bHasSex; } ... void CvUnitInfo::read(FDataStreamBase* stream) { CvHotkeyInfo::read(stream); uint uiFlag=0; stream->Read(&uiFlag); // flags for expansion stream->Read(&m_iAIWeight); stream->Read(&m_iProductionCost); ... stream->Read(&m_bHasSex); ... } ... void CvUnitInfo::write(FDataStreamBase* stream) { CvHotkeyInfo::write(stream); uint uiFlag=0; stream->Write(uiFlag); // flag for expansion stream->Write(m_iAIWeight); stream->Write(m_iProductionCost); ... stream->Write(m_bHasSex); ... } ... bool CvUnitInfo::read(CvXMLLoadUtility* pXML) { ... pXML->GetChildXmlValByName(&m_bHasSex, "bHasSex"); ...
続いて、ユニットクラスに性別フィールドを加えます。CvEnum.hで性別型定義を、CvUnitに性別フィールド・アクセサ・初期化コードを普通に書きます。
... enum DllExport SexTypes // Exposed to Python { NO_SEX = -1, SEX_MALE, SEX_FEMALE }; ...
... public: DllExport bool hasSex() const; // Exposed to Python DllExport SexTypes getSex() const; // Exposed to Python DllExport void setSex(SexTypes eSex); // Exposed to Python protected: SexTypes m_eSexType; void setRandomSex(); ...
... void CvUnit::reset(int iID, UnitTypes eUnit, PlayerTypes eOwner, bool bConstructorCall) { ... setRandomSex(); ... } ... bool CvUnit::hasSex() const { return m_eSexType != NO_SEX; } SexTypes CvUnit::getSex() const { return m_eSexType; } void CvUnit::setSex(SexTypes eSex) { m_eSexType = eSex; } void CvUnit::setRandomSex() { if (m_eUnitType == NO_UNIT || !m_pUnitInfo->hasSex()) m_eSexType = NO_SEX; else m_eSexType = GC.getGameINLINE().getSorenRandNum(2, "Sex selection") == 0 ? SEX_MALE : SEX_FEMALE; } ...
最後の行について。CvGame::getSorenRandNum(int iNum, const char* pszLog) は0以上iNum未満のランダムなintを返します。第2引数にはロギングのためランダム数の用途を書きます。
これでユニットに性別が付くようになりました。次はPythonからユニットオブジェクトの性別にアクセスできるようにします。ここまで編集したC++のクラス名はCvから始まっていましたが、これをCyに変えたものがPythonからDLLにアクセスするためのラッパークラスです。CyUnit に性別のアクセサを加えます。
public: ... bool hasSex(); int /* SexTypes */ CyUnit::getSex(); void setSex(int /* SexTypes */ eSex); ...
bool CyUnit::hasSex() { return m_pUnit ? m_pUnit->hasSex() : false; } int /* SexTypes */ CyUnit::getSex() { return m_pUnit ? (int)m_pUnit->getSex() : (int)NO_SEX; } void CyUnit::setSex(int /* SexTypes */ eSex) { if (m_pUnit) m_pUnit->setSex((SexTypes) eSex); }enumはintに変換しないといけないので注意しましょう。
... void CyUnitPythonInterface1(python::class_<CyUnit>& x) { OutputDebugString("Python Extension Module - CyUnitPythonInterface1\n"); x .def("isNone", &CyUnit::isNone, "bool () - Is this a valid unit instance?") .def("convert", &CyUnit::convert, "void (CyUnit* pUnit)") ... .def("hasSex", &CyUnit::hasSex, "bool ()") .def("getSex", &CyUnit::getSex, "SexTypes ()") .def("hasSex", &CyUnit::setSex, "void (SexTypes)") ...
... python::enum_<SexTypes>("SexTypes") .value("NO_SEX", NO_SEX) .value("SEX_MALE", SEX_MALE) .value("SEX_FEMALE", SEX_FEMALE) ; ...
... python::class_<CvUnitInfo, python::bases<CvInfoBase, CvScalableInfo> >("CvUnitInfo") .def("getAIWeight", &CvUnitInfo::getAIWeight, "int ()") .def("getProductionCost", &CvUnitInfo::getProductionCost, "int ()") .def("getHurryCostModifier", &CvUnitInfo::getHurryCostModifier, "int ()") ... .def("hasSex", &CvUnitInfo::hasSex, "bool ()") ...第3引数の文字列はメソッドの説明文です。もっときちんと書くとより良し。
メイン画面の左下のユニット表示欄をカスタマイズするため、C:\Program Files\CYBERFRONT\Sid Meier's Civilization 4(J)\Beyond the Sword(J)\Assets\Python\Screens\CvMainInterface.py を置き換えます。MODS\Assets\Python\Screens にコピーして編集。
... # >>> Sex MOD ##if (pHeadSelectedUnit.getHotKeyNumber() == -1): ## szBuffer = localText.getText("INTERFACE_PANE_UNIT_NAME", (pHeadSelectedUnit.getName(), )) ##else: ## szBuffer = localText.getText("INTERFACE_PANE_UNIT_NAME_HOT_KEY", (pHeadSelectedUnit.getHotKeyNumber(), pHeadSelectedUnit.getName())) szName = pHeadSelectedUnit.getName() if (pHeadSelectedUnit.hasSex()): if pHeadSelectedUnit.getSex() == SexTypes.SEX_MALE: szName += localText.getText("TXT_KEY_SEX_MALE", ()) elif pHeadSelectedUnit.getSex() == SexTypes.SEX_FEMALE: szName += localText.getText("TXT_KEY_SEX_FEMALE", ()) if (pHeadSelectedUnit.getHotKeyNumber() == -1): szBuffer = localText.getText("INTERFACE_PANE_UNIT_NAME", (szName, )) else: szBuffer = localText.getText("INTERFACE_PANE_UNIT_NAME_HOT_KEY", (pHeadSelectedUnit.getHotKeyNumber(), szName)) # <<< Sex MOD ...
<?xml version="1.0" encoding="ISO-8859-1"?> <Civ4GameText xmlns="http://www.firaxis.com"> <TEXT> <Tag>TXT_KEY_SEX_MALE</Tag> <English>m</English> <French>m</French> <German>m</German> <Italian>m</Italian> <Spanish>m</Spanish> <Japanese>♂</Japanese> </TEXT> <TEXT> <Tag>TXT_KEY_SEX_FEMALE</Tag> <English>f</English> <French>f</French> <German>f</German> <Italian>f</Italian> <Spanish>f</Spanish> <Japanese>♀</Japanese> </TEXT> </Civ4GameText>
これで完成です。ではゲームを起動してMODをロードしましょう。どこでつまづいたかを調べるのは結構大変なので、実際の開発の際は少しの修正ごとにこまめに確認することをお勧めします。
ジャガ男さんとジャガ子さんが麦畑
以上です。お疲れ様でした。