Ingame-Gif Creator
Ein Tastendruck – und die nächsten Sekunden Gameplay landen als animiertes GIF im Projektordner
Für RPG Maker MZ & MV · keine externen Bibliotheken · keine Plugin-Befehle
Du willst im Forum zeigen, woran du gerade arbeitest? Statt Bildschirmrekorder, Zuschneiden und Umwandeln drückst du einfach
F7: Das Plugin nimmt die nächsten Sekunden deines Spiels auf und speichert sie als fertiges GIF – ideal für Projektvorstellungen, Fortschrittsberichte, Bug-Meldungen oder Social Media.
Was das Plugin kann
- Aufnahme per Tastendruck: Standardmäßig F7 startet die Aufnahme, nach 5 Sekunden ist das GIF fertig. Ein erneuter Druck beendet die Aufnahme vorzeitig.
- Kein Ruckeln: Das GIF wird schon während der Aufnahme in einem Hintergrund-Thread (Web Worker) erstellt – das Spiel läuft flüssig weiter.
- Schöne Farben: Für jedes Bild wird eine eigene, optimale 256-Farben-Palette berechnet (Median-Cut).
- Saubere Bilder: Die kleine „REC“-Anzeige liegt über dem Spiel und ist im GIF nicht zu sehen.
- Komplett eigenständig: Der GIF-Encoder ist im Plugin enthalten – keine zusätzlichen Dateien oder Bibliotheken nötig.
- Läuft überall: Im Testspiel und in der Desktop-Version landet das GIF im Spielordner, in einer Browser-Version wird es stattdessen heruntergeladen.
Einbau in drei Schritten1. Den Code oben als
GifRecorder.js im Ordner
js/plugins/ deines Projekts speichern.
2. Im
Plugin-Manager hinzufügen und einschalten.
3. Testspiel starten,
F7 drücken – fertig.
Die GIFs findest du danach unter:
<Spielordner>/gifs/gameplay_JJJJ-MM-TT_HH-MM-SS.gif
Beim Testspielen aus dem Editor ist das dein Projektordner.
Der Dateiname muss genau GifRecorder.js lauten, sonst findet das Plugin seine Einstellungen nicht und nutzt die Standardwerte.
Einstellungen
| Parameter | Standard | Wirkung |
|---|
| Record Key | 118 (F7) | Tastencode der Aufnahmetaste – z. B. 119 = F8, 120 = F9, 82 = R |
| Duration | 5 Sekunden | Länge der Aufnahme (ab 0,5 Sekunden) |
| Frames per Second | 20 | Bildrate des GIFs – höher = flüssiger, aber größer. 15–25 ist ein guter Bereich. |
| Scale | 50 % | Größe des GIFs im Verhältnis zum Spielbild – 100 % ergibt sehr große Dateien |
| Output Folder | gifs | Ordner im Spielverzeichnis, in dem die GIFs gespeichert werden |
| Show REC Indicator | an | Zeigt während der Aufnahme eine kleine Anzeige (nicht im GIF enthalten) |
Tipp für Forenbeiträge: Mit Scale 50 % und 15–20 FPS bleiben die GIFs klein genug zum Hochladen und sehen trotzdem gut aus. Für Detailaufnahmen lieber die Dauer kürzen als die Größe erhöhen.
//=============================================================================
// GifRecorder.js
//=============================================================================
/*:
* @target MZ
* @plugindesc v1.0.0 Records the next seconds of gameplay as an animated GIF at the press of a key.
* @author cdqgrayde
*
* @param KeyCode
* @text Record Key (key code)
* @desc JavaScript key code of the record key. 118 = F7, 119 = F8, 120 = F9, 82 = R.
* @type number
* @default 118
*
* @param Duration
* @text Duration (seconds)
* @type number
* @decimals 1
* @min 0.5
* @default 5
*
* @param Fps
* @text Frames per Second
* @desc GIF frame rate. Higher = smoother but bigger file. 15-25 is a good range.
* @type number
* @min 1
* @max 50
* @default 20
*
* @param Scale
* @text Scale (%)
* @desc Size of the GIF relative to the game screen. 100 = full size (big files).
* @type number
* @min 10
* @max 100
* @default 50
*
* @param Folder
* @text Output Folder
* @desc Folder (inside the game folder) the GIFs are saved to. In a browser build the GIF is downloaded instead.
* @default gifs
*
* @param ShowIndicator
* @text Show REC Indicator
* @desc Shows a small overlay while recording. It is NOT part of the GIF.
* @type boolean
* @default true
*
* @help
* ============================================================================
* GifRecorder (RPG Maker MZ / MV)
* ============================================================================
* Press the record key (default F7) and the next seconds of gameplay
* (default 5) are recorded and saved as an animated GIF:
*
* <game folder>/gifs/gameplay_YYYY-MM-DD_HH-MM-SS.gif
*
* When playtesting from the editor this is your project folder.
* In a web/browser deployment the GIF is downloaded instead.
*
* The GIF is encoded in a background thread while recording, so the game
* keeps running smoothly. No external libraries are needed.
*
* Pressing the key again while recording stops the recording early.
*
* No plugin commands.
*/
(() => {
"use strict";
const PLUGIN_NAME = (() => {
const script = document.currentScript;
const match = script && script.src.match(/([^/]+)\.js$/);
return match ? decodeURIComponent(match[1]) : "GifRecorder";
})();
const params = PluginManager.parameters(PLUGIN_NAME);
const num = (name, def) => {
const n = Number(params[name]);
return params[name] === undefined || params[name] === "" || isNaN(n) ? def : n;
};
const P = {
keyCode: num("KeyCode", 118),
duration: num("Duration", 5) * 1000,
fps: num("Fps", 20).clamp(1, 50),
scale: num("Scale", 50).clamp(10, 100) / 100,
folder: params["Folder"] || "gifs",
indicator: params["ShowIndicator"] !== "false"
};
const INPUT_NAME = "gifRecord";
Input.keyMapper[P.keyCode] = INPUT_NAME;
//-------------------------------------------------------------------------
// GIF encoder (runs inside a Web Worker)
//-------------------------------------------------------------------------
function workerMain() {
let width = 0;
let height = 0;
const chunks = [];
let frameCount = 0;
function ByteWriter() {
this.buf = new Uint8Array(1 << 16);
this.len = 0;
}
ByteWriter.prototype.byte = function(b) {
if (this.len >= this.buf.length) {
const n = new Uint8Array(this.buf.length * 2);
n.set(this.buf);
this.buf = n;
}
this.buf[this.len++] = b;
};
ByteWriter.prototype.word = function(w) {
this.byte(w & 0xff);
this.byte((w >> 8) & 0xff);
};
ByteWriter.prototype.str = function(s) {
for (let i = 0; i < s.length; i++) this.byte(s.charCodeAt(i));
};
ByteWriter.prototype.data = function() {
return this.buf.subarray(0, this.len);
};
// Median cut on a 15-bit color histogram. Returns palette + lookup.
function quantize(rgba) {
const hist = new Uint32Array(32768);
const sumR = new Uint32Array(32768);
const sumG = new Uint32Array(32768);
const sumB = new Uint32Array(32768);
for (let i = 0; i < rgba.length; i += 4) {
const r = rgba[i];
const g = rgba[i + 1];
const b = rgba[i + 2];
const k = ((r >> 3) << 10) | ((g >> 3) << 5) | (b >> 3);
hist[k]++;
sumR[k] += r;
sumG[k] += g;
sumB[k] += b;
}
const keys = [];
for (let k = 0; k < 32768; k++) if (hist[k]) keys.push(k);
const makeBox = list => {
let count = 0;
const min = [31, 31, 31];
const max = [0, 0, 0];
for (const k of list) {
const c = [(k >> 10) & 31, (k >> 5) & 31, k & 31];
for (let j = 0; j < 3; j++) {
if (c[j] < min[j]) min[j] = c[j];
if (c[j] > max[j]) max[j] = c[j];
}
count += hist[k];
}
let axis = 0;
let range = -1;
for (let j = 0; j < 3; j++) {
if (max[j] - min[j] > range) {
range = max[j] - min[j];
axis = j;
}
}
return { list, count, axis, range };
};
const boxes = [makeBox(keys)];
while (boxes.length < 256) {
let best = -1;
let bestScore = 0;
for (let i = 0; i < boxes.length; i++) {
const b = boxes[i];
const score = b.list.length > 1 ? b.range * b.count : 0;
if (score > bestScore) {
bestScore = score;
best = i;
}
}
if (best < 0) break;
const box = boxes[best];
const shift = 10 - box.axis * 5;
box.list.sort((a, b) => ((a >> shift) & 31) - ((b >> shift) & 31));
let acc = 0;
let cut = 1;
for (let i = 0; i < box.list.length - 1; i++) {
acc += hist[box.list[i]];
cut = i + 1;
if (acc >= box.count / 2) break;
}
boxes.splice(best, 1, makeBox(box.list.slice(0, cut)), makeBox(box.list.slice(cut)));
}
const palette = new Uint8Array(256 * 3);
const lut = new Uint8Array(32768);
boxes.forEach((box, i) => {
let r = 0;
let g = 0;
let b = 0;
for (const k of box.list) {
r += sumR[k];
g += sumG[k];
b += sumB[k];
lut[k] = i;
}
palette[i * 3] = Math.round(r / box.count);
palette[i * 3 + 1] = Math.round(g / box.count);
palette[i * 3 + 2] = Math.round(b / box.count);
});
const indices = new Uint8Array(rgba.length / 4);
for (let i = 0, p = 0; i < rgba.length; i += 4, p++) {
indices[p] = lut[((rgba[i] >> 3) << 10) | ((rgba[i + 1] >> 3) << 5) | (rgba[i + 2] >> 3)];
}
return { palette, indices };
}
// GIF LZW compression, written as data sub-blocks.
function writeLzw(out, indices, minCodeSize) {
const clearCode = 1 << minCodeSize;
const eoiCode = clearCode + 1;
let codeSize = minCodeSize + 1;
let nextCode = eoiCode + 1;
let table = new Map();
const bytes = new ByteWriter();
let cur = 0;
let curShift = 0;
const emit = code => {
cur |= code << curShift;
curShift += codeSize;
while (curShift >= 8) {
bytes.byte(cur & 0xff);
cur >>>= 8;
curShift -= 8;
}
};
emit(clearCode);
let prefix = indices[0];
for (let i = 1; i < indices.length; i++) {
const k = indices[i];
const key = (prefix << 8) | k;
const found = table.get(key);
if (found !== undefined) {
prefix = found;
continue;
}
emit(prefix);
if (nextCode === 4096) {
emit(clearCode);
nextCode = eoiCode + 1;
codeSize = minCodeSize + 1;
table = new Map();
} else {
if (nextCode >= 1 << codeSize) codeSize++;
table.set(key, nextCode++);
}
prefix = k;
}
emit(prefix);
emit(eoiCode);
if (curShift > 0) bytes.byte(cur & 0xff);
out.byte(minCodeSize);
const data = bytes.data();
for (let i = 0; i < data.length; i += 255) {
const n = Math.min(255, data.length - i);
out.byte(n);
for (let j = 0; j < n; j++) out.byte(data[i + j]);
}
out.byte(0);
}
function header() {
const out = new ByteWriter();
out.str("GIF89a");
out.word(width);
out.word(height);
out.byte(0x70); // no global color table, 8 bit color resolution
out.byte(0);
out.byte(0);
// Loop forever
out.byte(0x21);
out.byte(0xff);
out.byte(11);
out.str("NETSCAPE2.0");
out.byte(3);
out.byte(1);
out.word(0);
out.byte(0);
return out.data();
}
function frame(rgba, delayCs) {
const { palette, indices } = quantize(rgba);
const out = new ByteWriter();
// Graphic control extension
out.byte(0x21);
out.byte(0xf9);
out.byte(4);
out.byte(0x04); // disposal: do not dispose
out.word(Math.max(2, delayCs));
out.byte(0);
out.byte(0);
// Image descriptor with 256 color local table
out.byte(0x2c);
out.word(0);
out.word(0);
out.word(width);
out.word(height);
out.byte(0x87);
for (let i = 0; i < palette.length; i++) out.byte(palette[i]);
writeLzw(out, indices, 8);
return out.data();
}
self.onmessage = e => {
const msg = e.data;
if (msg.type === "start") {
width = msg.width;
height = msg.height;
chunks.length = 0;
frameCount = 0;
chunks.push(header());
} else if (msg.type === "frame") {
chunks.push(frame(new Uint8Array(msg.buffer), msg.delay));
frameCount++;
} else if (msg.type === "finish") {
chunks.push(new Uint8Array([0x3b]));
let total = 0;
for (const c of chunks) total += c.length;
const result = new Uint8Array(total);
let offset = 0;
for (const c of chunks) {
result.set(c, offset);
offset += c.length;
}
chunks.length = 0;
self.postMessage({ type: "done", buffer: result.buffer, frames: frameCount }, [result.buffer]);
}
};
}
//-------------------------------------------------------------------------
// GifRecorder
//-------------------------------------------------------------------------
const GifRecorder = {
_worker: null,
_recording: false,
_encoding: false,
_canvas: null,
_context: null,
_startTime: 0,
_pending: null,
_indicator: null,
_hideTimer: null
};
window.GifRecorder = GifRecorder;
GifRecorder.worker = function() {
if (!this._worker) {
const code = "(" + workerMain.toString() + ")();";
const url = URL.createObjectURL(new Blob([code], { type: "text/javascript" }));
this._worker = new Worker(url);
this._worker.onmessage = e => this.onWorkerMessage(e.data);
this._worker.onerror = e => {
console.error("GifRecorder:", e.message);
this._encoding = false;
this.showIndicator("GIF-Fehler (siehe Konsole)", "#c33", 4000);
};
}
return this._worker;
};
GifRecorder.toggle = function() {
if (this._recording) {
this.stop();
} else if (!this._encoding) {
this.start();
}
};
GifRecorder.start = function() {
const source = Graphics._canvas;
if (!source) return;
const w = Math.max(1, Math.round(source.width * P.scale));
const h = Math.max(1, Math.round(source.height * P.scale));
this._canvas = document.createElement("canvas");
this._canvas.width = w;
this._canvas.height = h;
this._context = this._canvas.getContext("2d", { willReadFrequently: true });
this._context.imageSmoothingEnabled = true;
this.worker().postMessage({ type: "start", width: w, height: h });
this._recording = true;
this._startTime = performance.now();
this._pending = null;
};
GifRecorder.stop = function() {
if (!this._recording) return;
this._recording = false;
this.flushPending(1000 / P.fps);
this._encoding = true;
this.showIndicator("GIF wird erstellt …", "#d80", 0);
this.worker().postMessage({ type: "finish" });
};
// Called right after the game rendered a frame.
GifRecorder.capture = function() {
if (!this._recording) return;
const now = performance.now();
const elapsed = now - this._startTime;
if (elapsed >= P.duration) {
this.stop();
return;
}
if (this._pending && now - this._pending.time < 1000 / P.fps - 1) {
this.updateRecIndicator(elapsed);
return;
}
const w = this._canvas.width;
const h = this._canvas.height;
this._context.drawImage(Graphics._canvas, 0, 0, w, h);
const data = this._context.getImageData(0, 0, w, h).data;
this.flushPending(now - (this._pending ? this._pending.time : now));
this._pending = { data: data, time: now };
this.updateRecIndicator(elapsed);
};
// A frame is sent once we know how long it stays on screen.
GifRecorder.flushPending = function(durationMs) {
if (!this._pending) return;
const buffer = this._pending.data.buffer;
const delay = Math.round(durationMs / 10);
this.worker().postMessage({ type: "frame", buffer: buffer, delay: delay }, [buffer]);
this._pending = null;
};
GifRecorder.onWorkerMessage = function(msg) {
if (msg.type !== "done") return;
this._encoding = false;
const bytes = new Uint8Array(msg.buffer);
const name = "gameplay_" + this.timestamp() + ".gif";
try {
const file = this.save(name, bytes);
this.showIndicator("GIF gespeichert: " + file, "#2a2", 4000);
} catch (e) {
console.error("GifRecorder:", e);
this.showIndicator("GIF konnte nicht gespeichert werden", "#c33", 4000);
}
};
GifRecorder.save = function(name, bytes) {
if (Utils.isNwjs()) {
const fs = require("fs");
const path = require("path");
const base = path.dirname(process.mainModule.filename);
const dir = path.join(base, P.folder);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(path.join(dir, name), Buffer.from(bytes));
return P.folder + "/" + name;
}
const url = URL.createObjectURL(new Blob([bytes], { type: "image/gif" }));
const a = document.createElement("a");
a.href = url;
a.download = name;
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(url), 10000);
return name;
};
GifRecorder.timestamp = function() {
const d = new Date();
const p = n => String(n).padStart(2, "0");
return (
d.getFullYear() + "-" + p(d.getMonth() + 1) + "-" + p(d.getDate()) +
"_" + p(d.getHours()) + "-" + p(d.getMinutes()) + "-" + p(d.getSeconds())
);
};
//-------------------------------------------------------------------------
// Overlay (DOM element above the canvas, so it is never recorded)
//-------------------------------------------------------------------------
GifRecorder.showIndicator = function(text, color, hideAfterMs) {
if (!P.indicator) return;
if (!this._indicator) {
const el = document.createElement("div");
el.style.cssText =
"position:fixed;top:12px;left:12px;z-index:1000;padding:4px 10px;" +
"border-radius:4px;font:bold 14px sans-serif;color:#fff;" +
"pointer-events:none;background:rgba(0,0,0,0.6);";
document.body.appendChild(el);
this._indicator = el;
}
clearTimeout(this._hideTimer);
this._indicator.style.display = "block";
this._indicator.style.borderLeft = "6px solid " + color;
this._indicator.textContent = text;
if (hideAfterMs > 0) {
this._hideTimer = setTimeout(() => {
this._indicator.style.display = "none";
}, hideAfterMs);
}
};
GifRecorder.updateRecIndicator = function(elapsed) {
const left = Math.max(0, (P.duration - elapsed) / 1000);
this.showIndicator("● REC " + left.toFixed(1) + "s", "#e22", 0);
};
//-------------------------------------------------------------------------
// Hooks
//-------------------------------------------------------------------------
const _SceneManager_updateMain = SceneManager.updateMain;
SceneManager.updateMain = function() {
_SceneManager_updateMain.apply(this, arguments);
if (Input.isTriggered(INPUT_NAME)) {
GifRecorder.toggle();
}
};
if (Graphics._onTick) {
// MZ: render happens inside _onTick
const _Graphics_onTick = Graphics._onTick;
Graphics._onTick = function(deltaTime) {
_Graphics_onTick.call(this, deltaTime);
GifRecorder.capture();
};
} else if (Graphics.render) {
// MV
const _Graphics_render = Graphics.render;
Graphics.render = function(stage) {
_Graphics_render.call(this, stage);
GifRecorder.capture();
};
}
})();