Simutrans の実績システムは、プレイヤーの達成や特定の行動に対して報酬を与える機能です。現在は Steam 版で実装されていますが、将来的には独自の実績システムも計画されています。
実績システムは、以下のような目的で使用されます:
simachievements_t (抽象層)
↓
steam_achievements_t (Steam API 実装)
↓
Steam Backend
関連ファイル:
src/simutrans/simachievements.cc - 実績チェックロジックsrc/simutrans/simachievements.h - 実績システムのインターフェースsrc/simutrans/simachenum.h - 実績の列挙定義(X Macro 使用)src/steam/achievements.cc - Steam API との統合src/steam/achievements.h - Steam 実績データ構造実績の定義は X Macro パターンを使用しており、効率的な管理が可能です:
// simachenum.h
#define ACHIEVEMENTS \
X(ACH_LOAD_PAK192_COMIC) \
X(ACH_QUERY_DICTACTOR) \
X(ACH_PROD_INK) \
X(ACH_TOOL_REMOVE_BUSY_BRIDGE)
// enum 生成
#define X(id) id,
enum simachievements_enum { ACHIEVEMENTS };
#undef X
この方法により、1 か所で定義を変更するだけで、複数の場所に自動的に反映されます。
トリガー: 特定の Pakset をロードした時
実装場所: simachievements_t::check_pakset_ach()
例:
void simachievements_t::check_pakset_ach() {
std::string pak_name = env_t::pak_name;
pak_name.erase(pak_name.length() - 1);
if (STRICMP(pak_name.c_str(), "pak192.comic") == 0) {
set_achievement(ACH_LOAD_PAK192_COMIC);
}
}
現在の実績:
| 実績 ID | 説明 | 条件 |
|---|---|---|
ACH_LOAD_PAK192_COMIC |
pak192.comic を発見 | pak192.comic をロード |
用途:
トリガー: 特定のオブジェクトを調査(クエリ)した時
実装場所: simachievements_t::check_query_ach(const char* object_name)
例:
void simachievements_t::check_query_ach(const char* object_name) {
std::string pak_name = env_t::pak_name;
pak_name.erase(pak_name.length() - 1);
if ((STRICMP(pak_name.c_str(), "pak128") == 0) &&
strstr(object_name, "rmax_dictator_statue")) {
set_achievement(ACH_QUERY_DICTACTOR);
}
}
現在の実績:
| 実績 ID | 説明 | 条件 |
|---|---|---|
ACH_QUERY_DICTACTOR |
独裁者の像を発見 | pak128 で独裁者の像をクエリ |
用途:
トリガー: ゲームの特定の状態を達成した時
実装場所: simachievements_t::check_state_ach(karte_t* world)
例:
void simachievements_t::check_state_ach(karte_t* world) {
if (env_t::networkmode)
return; // マルチプレイヤーでは実績を無効化
std::string pak_name = env_t::pak_name;
pak_name.erase(pak_name.length() - 1);
for (fabrik_t* fab : world->get_fab_list()) {
if (STRICMP(pak_name.c_str(), "pak192.comic") == 0) {
if (check_yearly_production(fab, "Mr. Kraken", 0)) {
set_achievement(ACH_PROD_INK);
}
}
}
}
現在の実績:
| 実績 ID | 説明 | 条件 |
|---|---|---|
ACH_PROD_INK |
インクを大量生産 | pak192.comic で "Mr. Kraken" 工場が目標生産量達成 |
ACH_TOOL_REMOVE_BUSY_BRIDGE |
使用中の橋を削除 | 車両が通行中の橋を削除 |
生産量チェックヘルパー関数:
bool check_yearly_production(
fabrik_t* fab,
const char* name,
uint32 product_index,
double target_percent = 0.70 // デフォルトは最大生産の 70%
) {
// 工場名が一致するか確認
// 指定された製品の年間生産量を取得
// 目標生産量と比較
return yearly_production >= target_production;
}
用途:
新しい実績を追加するには、以下の手順を実行します:
simachenum.h)// 実績の総数を更新
#define NUM_ACHIEVEMENTS 5 // 新しい実績を追加したので +1
// X Macro に新しい実績を追加
#define ACHIEVEMENTS \
X(ACH_LOAD_PAK192_COMIC) \
X(ACH_QUERY_DICTACTOR) \
X(ACH_PROD_INK) \
X(ACH_TOOL_REMOVE_BUSY_BRIDGE) \
X(ACH_MY_NEW_ACHIEVEMENT) // 新しい実績
simachievements.cc)適切なチェック関数に条件を追加:
void simachievements_t::check_state_ach(karte_t* world) {
if (env_t::networkmode)
return;
// 新しい実績の条件チェック
if (/* 条件 */) {
set_achievement(ACH_MY_NEW_ACHIEVEMENT);
}
}
// 例: メインループ内で定期的にチェック
simachievements_t::check_state_ach(world);
// 例: 特定のアクション後にチェック
if (action_performed) {
simachievements_t::check_query_ach(object_name);
}
Steam 版では、steam_achievements_t クラスが Steam API と通信します:
class steam_achievements_t {
private:
uint64 app_id;
achievement_t *achievements;
bool stats_initialized;
std::vector<int> achievements_queue; // オフライン時のキュー
public:
bool request_stats();
bool set_achievement(int ach_enum);
// Steam コールバック
STEAM_CALLBACK(on_user_stats_received, ...);
STEAM_CALLBACK(on_user_stats_stored, ...);
STEAM_CALLBACK(on_achievement_stored, ...);
};
struct achievement_t {
int m_eAchievementID; // 内部 ID
const char *m_pchAchievementID; // Steam API ID
char m_rgchName[128]; // 実績名
char m_rgchDescription[256]; // 説明
bool m_bAchieved; // 解除済みフラグ
int m_iIconImage; // アイコンハンドル
};
Steam API が利用できない場合、実績はキューに保存され、次回オンライン時に同期されます:
bool steam_achievements_t::set_achievement(int ach_enum) {
if (!stats_initialized) {
// オフラインの場合はキューに追加
achievements_queue.push_back(ach_enum);
return false;
}
// Steam API を使って実績を解除
return SteamUserStats()->SetAchievement(
achievements[ach_enum].m_pchAchievementID
);
}
マルチプレイヤーゲームでは、状態ベースの実績は無効化されます:
void simachievements_t::check_state_ach(karte_t* world) {
if (env_t::networkmode)
return; // マルチプレイヤーでは実行しない
// 実績チェックのロジック...
}
理由:
以下のタイプの実績は、マルチプレイヤーでも許可されます:
実績の解除時にログが出力されます:
void simachievements_t::set_achievement(simachievements_enum ach) {
dbg->message("simachievements_t::set_achievement()",
"Unlocking achievement %d", ach);
#ifdef STEAM_BUILT
steam_t::get_instance()->get_achievements()->set_achievement(ach);
#endif
}
Steam の実績テストには、以下のツールを使用します:
SteamUserStats() の呼び出しをトレース非 Steam ビルドでは、ログメッセージのみが出力されます:
# デバッグログを有効化して起動
./simutrans -debug 3 -log 1
# ログファイルを確認
tail -f simutrans.log | grep achievement
コード内のコメントには、将来的な独自実績システムの実装が示唆されています:
/**
* Manages the achievement system. This currently only works for Steam achievements.
* TODO: It would be cool to have our own achievement system!
*/
class simachievements_t {
// ...
};
想定される機能:
シナリオやチュートリアルから実績を解除できるようにする計画もあります:
// TODO: A function to set achievement from script tools
// (for scenarios, tutorial, etc...)
これにより、Squirrel スクリプトから実績を制御可能になります:
// 想定される Squirrel API
achievement.unlock("ACH_TUTORIAL_COMPLETE");
achievement.check_progress("ACH_CONNECT_CITIES", 5, 10);
様々なプレイスタイルに対応した実績を用意:
特定の Pakset でのみ解除できる実績は、その旨を明記:
// pak192.comic でのみ解除可能
if (STRICMP(pak_name.c_str(), "pak192.comic") == 0) {
// 実績チェック
}
実績チェックは頻繁に実行されるため、パフォーマンスに注意:
| 実績 | 解除率 | 難易度 |
|---|---|---|
| 最初の路線を開設 | 95% | 簡単 |
| 10 都市を接続 | 60% | 中 |
| 月間収益 100 万 | 30% | 難 |
| すべての商品を輸送 | 5% | 非常に難 |
原因:
対処法:
// Steam の初期化を確認
if (steam_t::get_instance()) {
// Steam が利用可能
}
// デバッグログを確認
dbg->message("achievement", "Condition met: %d", condition_result);
原因: 同じ実績を複数回 set_achievement() で呼び出している
対処法: Steam API は自動的に重複を防ぐため、通常は問題ありません。
src/simutrans/simachievements.{cc,h}src/simutrans/simachenum.hsrc/steam/achievements.{cc,h}src/steam/steam.{cc,h}Simutrans の実績システムは、プレイヤーのエンゲージメントを高め、ゲームの多様な要素を発見させる効果的な機能です。現在は Steam 版で実装されていますが、将来的には独自システムの追加も計画されており、さらなる拡張が期待されます。
実績の追加は比較的簡単で、X Macro パターンにより保守性も高く保たれています。新しい実績のアイデアがあれば、コミュニティで提案してみてください!