前置き
私は個人でゲームを開発している暇人です。
とある日、Discordで自分のゲームを遊んでいる人が見えたらうれしいなと思い、Discord RichPresenceを実装しようと思いました。
そこで、Discord-RPCというラッパーライブラリを使用しようと思いましたが、どうやらこのDiscord-RPCというプロジェクト、ラップされている側のDiscord-RPC(dll的な奴)がすでにメンテナンスされていないようです。
どうしようかなとDiscordのDeveloper Guideを眺めていたらDiscord Game SDKというものが存在することを知りました。
「絶対これはいける!!」
そう確信した私はDiscord-Game-SDK4Jというラッパーライブラリを探し当てたのでした...
本題
今回はDiscord-Game-SDK4Jというライブラリを使用させてもらいます。
サンプルを見る感じ、このライブラリはDiscord-Game-SDKの.dll .so .dylib(以下ネイティブライブラリ)に対応しているそうです。
すなわちWindows、Linux、MacOSということですね。
こちらのライブラリなんですが、ネイティブライブラリを手動で用意する必要があるようです。
自動でダウンロードして配置してくれるユティリティークラスを作ったので、今回はこれを使用します。
このソースはMITライセンスで公開されているのでどなたでも基本自由に使用することができますが、使用する際はクレジット表記の方、お願いします。
ネーミングは適当です。
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
public class DiscordNativeFileUtil {
public static File getNativeFile() throws IOException {
String ext; // ネイティブファイルの拡張子、OSによって変わる。
File dir = new File(System.getProperty("java.io.tmpdir"), "discord-game-sdk4j");
String os = System.getProperty("os.name").toLowerCase();
String arch = System.getProperty("os.arch").toLowerCase();
if (arch.equals("amd64")) arch = "x86_64";
if (os.contains("windows"))
ext = ".dll";
else if (os.contains("linux"))
ext = ".so";
else if (os.contains("mac os")) ext = ".dylib";
else throw new RuntimeException("Not Supported OS Type: " + os);
if ((new File(dir, "discord_game_sdk"+ext).exists())) {
return new File(dir, "discord_game_sdk"+ext);
}
String nativePath = "lib/"+arch+"/discord_game_sdk"+ext;
URL download = new URL("https://dl-game-sdk.discordapp.net/2.5.6/discord_game_sdk.zip");
ZipInputStream input = new ZipInputStream(download.openStream());
ZipEntry entry;
while ((entry = input.getNextEntry()) != null) {
if (entry.getName().equals(nativePath)) {
if (!dir.mkdir()) {
throw new IOException("Cannot create directory.");
}
File nativeLib = new File(dir, "discord_game_sdk"+ext);
Files.copy(input, nativeLib.toPath());
input.close();
return nativeLib;
}
input.closeEntry();
}
input.close();
return null;
}
}
このコードはdiscord-game-sdk4j公式のサンプルを参考に作成しました。
一応ディスク上にキャッシュするようにしました(はずです)。
次にDiscord-Game-SDK4Jをビルドパスに追加します。
Mavenなら
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
<dependency>
<groupId>com.github.JnCrMx</groupId>
<artifactId>discord-game-sdk4j</artifactId>
<!-- 記事投稿時点での最新版は0.5.5です !-->
<version>v0.5.5</version>
</dependency>
それ以外ならこのページを参考にしてください。
次にRich Presenceを実際に実装していきます。
まずDiscord Developer PortalでRich Presenceのためのアプリケーションを作成します。
New Applicationを押します。
名前を入れてCreateを押します。
左のタブでRich Presenceを押します。
Cover Imageを任意の画像に設定します。(これはよくわからないです)
Add Imageを押して、mainとかその画像の役割を的確に表す素晴らしいネーミングを施した画像を追加しましょう。
完了すると上の画像みたいになります。
次にGeneral Informationタブに移動します。
Application IDをメモしておいてください(後で使います)
次はコード側です。
下のコードはラッパーのセットアップのためのコードです。
詳しくはコメントを見てもらえれば...
public class DiscordRP {
private Core core = null;
private boolean running = true;
private static final DiscordRP INSTANCE = new DiscordRP();
private DiscordRP() {}
private void Isetup() {
File nativeLib = DiscordNativeFileUtil.getNativeFile();
if (nativeLib == null) {
System.err.println("Discord Game SDKのダウンロードでエラーが発生しました。");
}
Core.init(nativeLib); //ここでCoreにラップ対象のネイティブライブラリのファイルを注入する、でいいのかな?
CreateParams params = new CreateParams();
params.setClientID(ここにApplicationIDを入れます); //ここはlong形で記述します 000000000000000000L みたいな感じ。
params.setFlags(CreateParams.getDefaultFlags());
core = new Core(params);
new Thread("DiscordGameSDK4J Callback") { //Discord Game SDKのコールバック関数を呼び出し続けるスレッド
@Override
public void run() {
while (running) {
core.runCallbacks();
}
}
}.start();
}
public static void setup() {
INSTANCE.Isetup();
}
public static Core getCore() {
return INSTANCE.core;
}
public static void shutdown() {
INSTANCE.running = false;
}
}
無理やりシングルトンにするためにめんどくさい記述方法をしてますが、普通のコードです。汚いのは許してください
使用するときは下のように呼び出します。
DiscordRP.setup(); //このコードはゲームの初期化処理で一回呼び出してください。
Core core = DiscordRP.getCore(); //これでセットアップされたCoreクラスがゲットできます。 Coreクラス!! ゲットだぜ!!!
次にActivity(Rich Presence)を作成するコードです。
Activity activity = new Activity();
activity.setDetails("Rich Presenceの一行目"); //一行目
activity.setState("Rich Presenceの二行目"); //二行目
activity.timestamps().setStart(Instant.now()); //Activityの開始時間を指定する 一番最初にInstantをインスタンス化しておいて、それを使いまわすと更新のたびに時間がリセットされることはなくなる。
activity.assets().setLargeImage("main"); //さっきDiscord Developer Portalでつけたイカした名前を指定する。
DiscordRP.getCore().activityManager().updateActivity(activity);
これでRich Presenceを実装することができます。
追記
このままだと永遠にコールバック用のスレッドが裏で動き続けるので必ず下のコードを終了時に呼び出してください。
DiscordRP.shutdown();
追記の追記
デーモンスレッドにしていないのでshutdown()を呼ばなくても仕様上は終了されるっぽいです。
でも気持ち悪いのでちゃんと明示的に終了するようにしておきます。