Hallo Leute,
ich hab früher viel mit dem RMXP gemacht. Hauptsächlich Scripts. Unter anderem auch ein eigenes Framework mit dem Ziel, den Einschränkungen des RMXP Herr zu werden. Eigentlich ist es schade, dass viel von dem Zeug jetzt auf irgendeiner Harddisk vor sich hin gammelt. Also möchte ich versuchen, wann immer ich Zeit habe, einzelne Teile daraus zu extrahieren und hier als Stand Alone Open Source Scripts anzubieten.
Das Lizenz Modell
Das nachfolgende Lizenzmodell soll für alle meine Publikationen im Rahmen dieses Posts gelten:
Copyright 2018 - 2028 agenty <agenty1@gmx.de>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Credits
Früher war es üblich, die Macher von Scripts in den Credits des Spiels zu erwähnen. Ich habe schon immer davon abgesehen, die Nutzer meiner Scripts dazu zu zwingen. (Maßanfertigungen wären ggf. nochmal eine andere Sache) Daher besteht keine Pflicht, mich zu erwähnen. Im Gegenteil, wenn mich jemand namentlich erwähnen möchte, dann bitte nur mit Absprache.
Scripts
Hier werden nach und nach, immer wenn ich Zeit habe, die Scripts angehängt werden.
Dieses Script ermöglicht das abfangen von Tastatur Eingaben, inklusive des zugehörigen Buchstabens und des Namens der Taste. Das Ganze funktioniert über eine DLL. Ich veröffentliche hier den kompletten sourcecode statt der Files. Getestet hab ichs auf dem MinGW x86.
Header
- #ifndef POLL_KEYBOARD
- #define POLL_KEYBOARD __declspec(dllexport)
- #include <Windows.h>
- //! Windows Low Level API hook callback function for keyboard messages.
- LRESULT __stdcall keyevent(int nCode, WPARAM wParam, LPARAM lParam);
- extern "C" {
- //! The structure for a keyboard event response.
- struct keyboard_response {
- int keycode; /*!< The virtual key code. */
- //! The message type.
- /*!
- * 0: Key Down
- * 1: Key Up
- * 2: System Key Down
- * 3: System Key Up
- */
- int type;
- int letter_size; /*!< Number of chars for the captured letter. */
- char letter[16]; /*!< The representation of the letter, which was pressed. */
- int name_size; /*!< Number of chars for the pressed key name. */
- char name[256]; /*!< The key name. */
- };
- //! Initialize the keyboard.
- /*
- * Allocates a std::queue to enqueue keyboard responses and registers the callback.
- */
- void init_keyboard(void);
- //! Dequeues from the queue and parses the response to the given pointer.
- /*
- * Return Values:
- * -1: The queue was empty.
- * 0: The queue had at least one element, but the key has no letter.
- * 1: The queue had at least one element and the message contained a key code, that is convertible to a letter.
- */
- int update_keyboard(keyboard_response * response);
- //! Frees the allocated queue memory and unregisters the callback.
- void dispose_keyboard(void);
- }
- #endif
Source
- #include <Keyboard.hpp>
- #include <queue>
- inline void parse_type(keyboard_response & response, WPARAM & wParam);
- inline void parse_char(keyboard_response & response, tagKBDLLHOOKSTRUCT & param);
- inline void parse_key_name(keyboard_response & response, UCHAR virtualKey);
- std::queue<keyboard_response> * g_keyboard_queue;
- HHOOK g_keyboardhook;
- HWND g_keyboard_window;
- LRESULT __stdcall keyevent(int nCode, WPARAM wParam, LPARAM lParam) {
- HWND current_window = GetActiveWindow();
- if(g_keyboard_window == current_window) {
- keyboard_response response;
- tagKBDLLHOOKSTRUCT param = *((tagKBDLLHOOKSTRUCT*)lParam);
- response.keycode = param.vkCode;
- parse_type(response,wParam);
- parse_char(response,param);
- parse_key_name(response,param.vkCode);
- g_keyboard_queue -> push(response);
- }
- return 0;
- }
- POLL_KEYBOARD void init_keyboard(void) {
- g_keyboard_queue = new std::queue<keyboard_response>();
- g_keyboard_window = GetActiveWindow();
- g_keyboardhook = SetWindowsHookEx(WH_KEYBOARD_LL,keyevent,NULL,0);
- }
- POLL_KEYBOARD int update_keyboard(keyboard_response * response) {
- if(g_keyboard_queue->empty()) {
- return -1;
- }
- keyboard_response element = g_keyboard_queue -> front();
- g_keyboard_queue -> pop();
- response -> keycode = element.keycode;
- response -> type = element.type;
- response -> letter_size = element.letter_size;
- for(int i = 0; i < element.letter_size; ++i) {
- response -> letter[i] = element.letter[i];
- }
- response -> name_size = element.name_size;
- for(int i = 0; i < element.name_size; ++i) {
- response -> name[i] = element.name[i];
- }
- return element.letter_size == 0 ? 0 : 1;
- }
- POLL_KEYBOARD void dispose_keyboard(void) {
- delete g_keyboard_queue;
- UnhookWindowsHookEx(g_keyboardhook);
- }
- inline void parse_type(keyboard_response & response, WPARAM & wParam) {
- if(WM_KEYDOWN == wParam) {
- response.type = 0;
- } else if(WM_KEYUP == wParam) {
- response.type = 1;
- } else if(WM_SYSKEYDOWN == wParam) {
- response.type = 2;
- } else {
- response.type = 3;
- }
- }
- inline void parse_char(keyboard_response & response, tagKBDLLHOOKSTRUCT & param) {
- BYTE kb[256];
- WCHAR uc[16] = {};
- GetKeyboardState(kb);
- response.letter_size = ToUnicode(param.vkCode, param.scanCode, kb, uc, 16, 0);
- if(response.letter_size > 0) {
- WideCharToMultiByte(CP_ACP,WC_COMPOSITECHECK,uc,16,response.letter,16,NULL,NULL);
- }
- }
- inline void parse_key_name(keyboard_response & response, UCHAR virtualKey) {
- UINT scanCode = MapVirtualKey(virtualKey, 0);
- int result = 0;
- switch (virtualKey) {
- case VK_LEFT: case VK_UP: case VK_RIGHT: case VK_DOWN:
- case VK_PRIOR: case VK_NEXT:
- case VK_END: case VK_HOME:
- case VK_INSERT: case VK_DELETE:
- case VK_DIVIDE:
- case VK_NUMLOCK:
- {
- scanCode |= 0x100;
- break;
- }
- }
- result = GetKeyNameTextA(scanCode << 16, response.name, sizeof(response.name));
- response.name_size = result;
- }
Ruby Wrapper
- # Keyboard Ruby part.
- class Keyboard
- # DLL Function Wrappper.
- # TODO: Replace the name of the DLL after you compiled it with the name of your DLL.
- INIT = Win32API.new("libPollKeyboard.dll","init_keyboard",['v'],'v')
- UPDATE = Win32API.new("libPollKeyboard.dll","update_keyboard",['p'],'l')
- DISPOSE = Win32API.new("libPollKeyboard.dll","dispose_keyboard",['v'],'v')
- SYNC_KEY = Win32API.new("user32","GetKeyState",['i'],'i')
- # Types of key events.
- DOWN = 0
- UP = 1
- SYSDOWN = 2
- SYSUP = 3
- # Keyboard Event Response Type.
- KeyEvent = Struct.new(:keycode,:letter,:name)
- # Map of esacaped chars.
- # TODO: Enhance for your needs.
- ESCAPE_TABLE = {
- "\344" => "ä",
- "\304" => "Ä",
- "\366" => "ö",
- "\326" => "Ö",
- "\374" => "ü",
- "\334" => "Ü",
- "\\" => '\\',
- "\200" => "€",
- "\265" => "µ",
- "\010" => ""
- }
- # Some Virtual Key Code Mappings.
- # Warning: May differ from System to System
- # TODO: Enhance for your needs.
- module Control
- RETURN = 0x0D
- ESCAPE = 0x1B
- LEFT = 0x25
- UP = 0x26
- RIGHT = 0x27
- DOWN = 0x28
- BACK = 0x08
- end
- # The Handler Interface.
- # Include this in your listener.
- module Handler
- def key_down(event)
- end
- def key_up(event)
- end
- def syskey_down(event)
- end
- def syskey_up(event)
- end
- end
- # Ctor.
- # Eats a list of Keyboard::Handler typed objects.
- def initialize(*observers)
- @observers = []
- observers.each {|obj| self << obj}
- INIT.call()
- end
- # Adds a listener to the keyboard instance.
- def add_observer(observer)
- raise(TypeError) unless observer.is_a?(Keyboard::Handler)
- @observers << observer
- self
- end
- alias << add_observer
- # Removes a listener from the keyboard instance.
- def remove_observer(observer)
- @observers.delete(observer)
- self
- end
- # Synchron update cycle.
- def update
- structure = " " * 672
- retval = UPDATE.call(structure)
- unless -1 == retval
- event = KeyEvent.new
- unpacked = structure.unpack('lllZ16lZ*')
- event.keycode = unpacked[0]
- type = unpacked[1]
- event.letter = unpacked[3][0,unpacked[2]]
- if ESCAPE_TABLE.keys.include?(event.letter)
- event.letter = ESCAPE_TABLE[event.letter]
- end
- event.name = unpacked[5][0,unpacked[4]]
- respond(type,event)
- end
- end
- # Dont forget to call this, when you are done!
- def dispose
- DISPOSE.call()
- end
- # Check if a key is pressed at the moment.
- def self.press?(keycode)
- !SYNC_KEY.call(keycode).between?(0, 1)
- end
- private
- def respond(type, event)
- @observers.each do |handler|
- case type
- when DOWN
- handler.key_down(event)
- when UP
- handler.key_up(event)
- when SYSDOWN
- handler.syskey_down(event)
- when SYSUP
- handler.syskey_up(event)
- end
- end
- end
- end
Beispiel
So nun noch ein kleines Anwendungsbeispiel, dass ich auf die schnelle hier im post zusammengestellt habe.
- class Scene_Test
- include Keyboard::Handler
- ESCAPE = 0x1B
- def key_up(event)
- if ESCAPE == event.keycode
- $game_system.se_play($data_system.cancel_se)
- $scene = Scene_Map.new
- return
- end
- end
- def main
- @sprite = Sprite.new
- @sprite.bitmap = Bitmap.new(640,480)
- @keyboard = Keyboard.new(self)
- Graphics.transition
- loop do
- Graphics.update
- Input.update
- update
- @keyboard.update
- if $scene != self
- break
- end
- end
- dispose
- end
- def update
- end
- def dispose
- @sprite.dispose
- @keyboard.dispose
- end
- end
Das folgende Script sorgt dafür, dass man die Maus nutzen kann.
Folgende Maus-Altionen werden erkannt:
- Linke Maustaste gedrückt
- Linke Maustaste losgelassen
- Mittlere Maustaste gedrückt
- Mittlere Maustaste losgelassen
- Rechte Maustaste gedrückt
- Rechte Maustaste losgelassen
- Mausrad nach vorne
- Mausrad nach hinten
Getestet hab ich das ganze mit dem MinGW x86.
Header
- #ifndef POLL_MOUSE
- #define POLL_MOUSE __declspec(dllexport)
- #include <Windows.h>
- //! Windows Low Level API hook callback function for mouse messages.
- LRESULT __stdcall mouseevent(int nCode, WPARAM wParam, LPARAM lParam);
- extern "C" {
- //! The structure for a mouse event response.
- struct mouse_response {
- //! The message type.
- /*!
- * 0: Left button down
- * 1: Left button up
- * 2: Middle button down
- * 3: Middle button up
- * 4: Right button down
- * 5: Right button up
- * 6: Mouse wheel forwards
- * 7: Mouse wheel backwards
- */
- int type;
- int absolute_x; /*!< Screen x coordinate. */
- int absolute_y; /*!< Screen y coordinate. */
- int relative_x; /*!< Window x coordinate. */
- int relative_y; /*!< Window y coordinate. */
- };
- //! The mouse position structure. */
- struct mouse_position {
- int absolute_x; /*!< Screen x coordinate. */
- int absolute_y; /*!< Screen y coordinate. */
- int relative_x; /*!< Window x coordinate. */
- int relative_y; /*!< Window y coordinate. */
- };
- //! Initialize the mouse.
- /*
- * Allocates a std::queue to enqueue mouse responses and registers the callback.
- */
- void init_mouse(void);
- //! Dequeues from the queue and parses the response to the given pointer.
- /*
- * Return Values:
- * -1: The queue was empty.
- * 0: The queue had at least one element.
- */
- int update_mouse(mouse_response * response);
- //! Frees the allocated queue memory and unregisters the callback.
- void dispose_mouse(void);
- //! Writes the current mouse position to the given pointer.
- void get_mouse_position(mouse_position * position);
- //! Hides the windows cursor in the context of the current window.
- void hide_mouse(void);
- //! Re-enables the windows cursor.
- void show_mouse(void);
- }
- #endif
Source
- #include <Mouse.hpp>
- #include <queue>
- inline int parse_type(WPARAM & wParam, MSLLHOOKSTRUCT & event);
- std::queue<mouse_response> * g_mouse_queue;
- HWND g_mouse_window;
- HHOOK g_mousehook;
- mouse_position g_mouse_position;
- LRESULT __stdcall mouseevent(int nCode, WPARAM wParam, LPARAM lParam) {
- HWND current_window = GetActiveWindow();
- MSLLHOOKSTRUCT event = *((MSLLHOOKSTRUCT*)lParam);
- if(g_mouse_window == current_window) {
- RECT window_rect;
- GetWindowRect(g_mouse_window,&window_rect);
- g_mouse_position.absolute_x = event.pt.x;
- g_mouse_position.absolute_y = event.pt.y;
- g_mouse_position.relative_x = event.pt.x - window_rect.left;
- g_mouse_position.relative_y = event.pt.y - window_rect.top;
- int type = parse_type(wParam,event);
- if (-1 != type) {
- mouse_response response;
- response.type = type;
- response.absolute_x = g_mouse_position.absolute_x;
- response.absolute_y = g_mouse_position.absolute_y;
- response.relative_x = g_mouse_position.relative_x;
- response.relative_y = g_mouse_position.relative_y;
- g_mouse_queue->push(response);
- }
- }
- return 0;
- }
- POLL_MOUSE void init_mouse(void) {
- g_mouse_queue = new std::queue<mouse_response>();
- g_mouse_window = GetActiveWindow();
- g_mousehook = SetWindowsHookEx(WH_MOUSE_LL,mouseevent,NULL,0);
- }
- POLL_MOUSE int update_mouse(mouse_response * response) {
- if(g_mouse_queue->empty()) {
- return -1;
- }
- mouse_response element = g_mouse_queue -> front();
- g_mouse_queue -> pop();
- response -> type = element.type;
- response -> absolute_x = element.absolute_x;
- response -> absolute_y = element.absolute_y;
- response -> relative_x = element.relative_x;
- response -> relative_y = element.relative_y;
- return 0;
- }
- POLL_MOUSE void dispose_mouse(void) {
- delete g_mouse_queue;
- UnhookWindowsHookEx(g_mousehook);
- }
- POLL_MOUSE void get_mouse_position(mouse_position * position) {
- position -> absolute_x = g_mouse_position.absolute_x;
- position -> absolute_y = g_mouse_position.absolute_y;
- position -> relative_x = g_mouse_position.relative_x;
- position -> relative_y = g_mouse_position.relative_y;
- }
- POLL_MOUSE void hide_mouse(void) {
- SetCapture(g_mouse_window);
- }
- POLL_MOUSE void show_mouse(void) {
- ReleaseCapture();
- }
- inline int parse_type(WPARAM & wParam, MSLLHOOKSTRUCT & event) {
- switch(wParam) {
- case WM_LBUTTONDOWN:
- return 0;
- case WM_LBUTTONUP:
- return 1;
- case WM_MBUTTONDOWN:
- return 2;
- case WM_MBUTTONUP:
- return 3;
- case WM_RBUTTONDOWN:
- return 4;
- case WM_RBUTTONUP:
- return 5;
- case WM_MOUSEWHEEL:
- short direction = GET_WHEEL_DELTA_WPARAM(event.mouseData);
- if(direction >= 0) {
- return 6;
- } else {
- return 7;
- }
- }
- return -1;
- }
Ruby Wrapper
- # Mouse Ruby Part
- class Mouse
- # DLL Function Wrappper.
- # TODO: Replace the name of the DLL after you compiled it with the name of your DLL.
- INIT = Win32API.new("libPollKeyboard.dll","init_mouse",['v'],'v')
- UPDATE = Win32API.new("libPollKeyboard.dll","update_mouse",['p'],'l')
- DISPOSE = Win32API.new("libPollKeyboard.dll","dispose_mouse",['v'],'v')
- POS = Win32API.new("libPollKeyboard.dll","get_mouse_position",['p'],'v')
- HIDE = Win32API.new("libPollKeyboard.dll","hide_mouse",['v'],'v')
- SHOW = Win32API.new("libPollKeyboard.dll","show_mouse",['v'],'v')
- # The Mouse Event response types.
- module Event_Type
- LBTN_DOWN = 0
- LBTN_UP = 1
- MBTN_DOWN = 2
- MBTN_UP = 3
- RBTN_DOWN = 4
- RBTN_UP = 5
- WHEEL_UP = 6
- WHEEL_DOWN = 7
- end
- # Ruby representation of the mouse position.
- MousePosition = Struct.new(:ax,:ay,:x,:y)
- # The Handler Interface.
- # Include this in your listener.
- module Handler
- def left_button_down(position)
- end
- def left_button_up(position)
- end
- def mid_button_down(position)
- end
- def mid_button_up(position)
- end
- def right_button_down(position)
- end
- def right_button_up(position)
- end
- def wheel_down(position)
- end
- def wheel_up(position)
- end
- end
- # Grants read access to the mouse position member.
- attr_reader :position
- # Ctor.
- # Eats a list of Mouse::Handler typed objects.
- def initialize(*observers)
- @observers = observers
- @position = MousePosition.new(0,0,0,0)
- INIT.call()
- @mouse_sprite = nil
- end
- # Adds a listener to the mouse instance.
- def add_observer(observer)
- @observers << observer
- self
- end
- alias << add_observer
- # Removes a listener from the mouse instance.
- def remove_observer(observer)
- @observers.delete(observer)
- self
- end
- # Sync update cycle.
- def update
- update_position
- response = " " * 160
- retval = UPDATE.call(response)
- unless -1 == retval
- pos = MousePosition.new(0,0,0,0)
- type,pos.ax,pos.ay,pos.x,pos.y = response.unpack('lllll')
- respond(type,pos)
- end
- if @mouse_sprite
- @mouse_sprite.x = self.position.x - (@mouse_sprite.bitmap.width / 2)
- @mouse_sprite.y = self.position.y - (@mouse_sprite.bitmap.height / 2)
- @mouse_sprite.update
- end
- end
- # Clears the mouse icon and sets it to a new icon if a bitmap was given.
- def cursor=(bitmap)
- if @mouse_sprite
- @mouse_sprite.dispose
- SHOW.call()
- end
- if bitmap.is_a?(Bitmap)
- @mouse_sprite = Sprite.new
- @mouse_sprite.bitmap = bitmap
- HIDE.call()
- end
- end
- # Dont forget to call this, when you are done!
- def dispose
- if @mouse_sprite
- @mouse_sprite.dispose
- SHOW.call()
- end
- DISPOSE.call()
- end
- private
- def update_position
- response = " " * 128
- POS.call(response)
- @position.ax,@position.ay,@position.x,@position.y = response.unpack('llll')
- end
- def respond(type,position)
- @observers.each do |ob|
- case type
- when Event_Type::LBTN_DOWN
- ob.left_button_down(position)
- break
- when Event_Type::LBTN_UP
- ob.left_button_up(position)
- break
- when Event_Type::MBTN_DOWN
- ob.mid_button_down(position)
- break
- when Event_Type::MBTN_UP
- ob.mid_button_up(position)
- break
- when Event_Type::RBTN_DOWN
- ob.right_button_down(position)
- break
- when Event_Type::RBTN_UP
- ob.right_button_up(position)
- break
- when Event_Type::WHEEL_UP
- ob.wheel_up(position)
- break
- when Event_Type::WHEEL_DOWN
- ob.wheel_down(position)
- break
- end
- end
- end
- end
Beispiel Implementierung
- class Scene_Test
- include Mouse::Handler
- def left_button_down(position)
- @text = "left down"
- end
- def left_button_up(position)
- @text = "left up"
- end
- def mid_button_down(position)
- @text = "mid down"
- end
- def mid_button_up(position)
- @text = "mid up"
- end
- def right_button_down(position)
- @text = "right down"
- end
- def right_button_up(position)
- @text = "right up"
- end
- def wheel_down(position)
- @text_y -= 3
- end
- def wheel_up(position)
- @text_y += 3
- end
- def main
- @sprite = Sprite.new
- @sprite.bitmap = Bitmap.new(640,480)
- @text = "Bitte Aktion tätigen"
- r = @sprite.bitmap.text_size(@text)
- @text_height = r.height
- @text_y = 210 - (r.height / 2)
- @mouse = Mouse.new(self)
- @mouse.cursor = RPG::Cache.icon("028-Herb04")
- Graphics.transition
- loop do
- Graphics.update
- Input.update
- update
- if $scene != self
- break
- end
- end
- dispose
- end
- def update
- @mouse.update
- update_text
- end
- def dispose
- @sprite.dispose
- @mouse.dispose
- end
- private
- def update_text
- @sprite.bitmap.clear
- str = "x: #{@mouse.position.x}, y: #{@mouse.position.y}, ax: #{@mouse.position.ax}, ay: #{@mouse.position.ay}"
- @sprite.bitmap.draw_text(0,0, 640, @text_height, str, 0)
- @sprite.bitmap.draw_text(0,@text_y, 640, @text_height, @text, 1)
- @sprite.update
- end
- end
Dieses Script erlaubt es, eine Log Datei zu führen. Dies dient dazu, das debugging zu erleichtern. Der Logger kennt 4 Level:
- Debug
- Info
- Warn
- Error
Logs des Loglevels "Debug" werden nur in die Datei geschrieben, wenn sich das Spiel im Debug Modus befindet, also über den Maker
gestartet wurde.
Über die Konstante Log kann auf die Funktionen zugegriffen werden. Die Funktionen heißen wie die Log-Level und verlangen einen tag und eine Nachricht.
Der Tag ist ein String und dient dazu, spezielle Einträge schneller wieder finden zu können. Die Nachricht kann entweder ein String oder eine Exception sein.
Achtung! Ich empfehle neben der Konstanten keine weiteren Instanzen zu erzeugen, um Komplikationen mit der Logdatei zu vermeiden.
Hinzu kommt ein JavaFX Programm mit dem man so erzeugte Log-Datein anzeigen und nach tag filtern kann.
Ruby Code
- class Logger
- PATH = "./logs"
- FILE = "1.log"
- module Level
- DEBUG = 0
- INFO = 1
- WARN = 2
- ERROR = 3
- end
- LEVEL_NAMES = ["DEBUG","INFO","WARN","ERROR"]
- def initialize
- unless File.directory?(PATH)
- Dir.mkdir(PATH)
- end
- unless File.exist?("#{PATH}/#{FILE}")
- File.open("#{PATH}/#{FILE}","w").close
- end
- end
- def debug(tag,message)
- log(Level::DEBUG,tag,message)
- self
- end
- def info(tag,message)
- log(Level::INFO,tag,message)
- self
- end
- def warn(tag,message)
- log(Level::WARN,tag,message)
- self
- end
- def error(tag,message)
- log(Level::ERROR,tag,message)
- self
- end
- private
- def log(level,tag,message)
- if level == Level::DEBUG && !$DEBUG
- return
- end
- time = Time.now.strftime("%d.%m.%Y %H:%M:%S")
- type = LEVEL_NAMES[level]
- value = message.is_a?(Exception) ? message.message : message.to_s
- out = "#{time}|#{type}|#{tag}|#{message}\n"
- File.open("#{PATH}/#{FILE}","a") do |f|
- f.puts out
- end
- end
- end
- Log = Logger.new
Log Viewer
Dieses Script stellt eine generische Schnittstelle zur Erweiterung von Scene_Map zur Verfügung.
Dabei kann diese Scene durch eine dem MVC Architekturmuster folgende Programmlogik ergänzt werden.
Warum das alles?
Viele Scripts die ich kenne beschäftigen sich mit Erweiterungen dieser Klasse. Kopiert man jetzt Script für Script in sein Spiel, dann aliased jedes dieser Scripts die Methoden der Klasse. Da sind Namenskonflikte vorprogrammiert.
Warum so kompliziert?
Das MVC pattern ist ein bewehrtes Muster für Anwendungen mit Benutzerinteraktion. Hierbei stehen Models (M) für Datenlogik, Views (V) für Anzeigelogik und Controller (C) für Steuerlogik. Das ist einfach eine logische Trennung von Aufgabenbereichen. Das fördert die Wartbarkeit eures codes. Wem das zu viel ist, der kann immer noch auf die Vorteile verzichten und nur, den Controller nutzen, ich rate allerdings davon ab.
Model
Das Model symbolisiert die Datenhaltung und die damit verbundenen Teile der Programmlogik. Hierzu zählt unter anderem die validierung der Daten. Models aus diesem Script enthalten Meta-Schnittstellen um im Klassenkopf Attribute zu erzeugen, die einen Standardwert und Validatoren besitzen können. Der Controller wiederum besitzt Meta-Schnittstellen um mehrere Models einzubinden.
- # Basic Model Class
- class Map_Model
- # Id public attribute.
- attr_accessor :id
- # Validates the attributes for the given names.
- # param attributes {Array} The list of attribute names.
- # return true if all given attributes are valid, else false.
- def validate(*attributes)
- if attributes.empty?
- return validate_all
- end
- retval = true
- attributes.each {|a| retval &= validate_attribute(a) }
- retval
- end
- # Validates all attributes.
- # return true if all attributes are valid, else false.
- def validate_all
- retval = true
- methods.
- map {|name| name.to_s}.
- select {|name| name[0,20] == "validate_model_attr_"}.
- each { |name| retval &= send(name.to_sym) }
- retval
- end
- # Validates one attribute.
- # param name {String|Symbol} The name of the attribute.
- # return true if the attribute is valid, else false.
- def validate_attribute(name)
- retval = false
- m = methods.detect {|n| n.to_s == "validate_model_attr_#{name}"}
- unless m.nil?
- retval = send(m)
- end
- retval
- end
- # Adds a new attribute.
- # Use this in your class header.
- # Defines the getter and setter methods for the attribute.
- # Defines the validation function for the attribute.
- # param name {Symbol} The name of the attribute.
- # param options {Hash} A Map of options:
- # default: The default value for the attribute
- # validate: A Map of validators to use
- # presence: Nil check
- # type: Type of the attribute
- # custom: A Block receiving the attribute, returning a boolean
- def self.model_attr(name,options={})
- var_name = "@model_attr_#{name}".to_sym
- default = options.keys.include?(:default) ? options[:default] : nil
- define_method(name.to_sym) do
- val = instance_variable_get(var_name)
- val = default if val.nil?
- val
- end
- define_method("#{name}=".to_sym) do |value|
- instance_variable_set(var_name,value)
- end
- valid = options[:validate]
- define_method("validate_model_attr_#{name}".to_sym) do
- retval = true
- if options.keys.include?(:validate)
- if valid.keys.include?(:presence)
- retval &= (!send(name.to_sym).nil? == valid[:presence])
- end
- if valid.keys.include?(:type)
- retval &= send(name.to_sym).is_a?(valid[:type])
- end
- if valid.keys.include?(:custom)
- retval &= valid[:custom].call(send(name.to_sym))
- end
- end
- retval
- end
- nil
- end
- end
View
Eine view stellt die Anzeige dar. Beispielsweise würde sie die Sprites managen und/oder die Benutzereingaben fangen und an den zugehörigen Controller weiterleiten. Für letzters besitzt die View dieses Scripts eine Meta-Schnittstelle um Topics zu erzeugen. Der zugehörige Controller wird dann beim eintreten der Bedingung des Topics informiert und kann so weitere Steueranweisungen tätigen. Auch für das einbinden einer View besitzt der Controller eine Meta-Schnittstelle. Zu jedem Controller gehört maximal eine View.
- # Basic view class
- class Map_View
- # Topic type.
- Topic = Struct.new(:name,:condition)
- # Topic cache.
- @@topics = {}
- # Ctor.
- def initialize
- if @@topics.keys.include?(self.class)
- @topics = @@topics[self.class]
- else
- @topics = []
- end
- @subscribes = []
- end
- # Construction complete hook.
- def init
- end
- # Update function.
- def update
- @topics.
- select {|t| @subscribes.include?(t.name)}.
- select {|t| t.condition.call}.
- each {|t| @controller.on_event(t.name)}
- on_update
- end
- # Post update hook.
- def on_update
- end
- # Called on dispose.
- def dispose
- end
- # Used by the controller to subscribe for a topic.
- # param topic {Symbol} The name of the topic.
- def subscribe(topic)
- @subscribes << topic
- self
- end
- # Request all models from the controller.
- # return {Map_Controller::Model_Wrapper} The models the controller knows.
- def models
- @controller.send(:models)
- end
- # Register a topic.
- # Use this in your class header.
- # param name {Symbol} The name of the topic.
- # param condition {Proc} A boolean function that returns true, if the topics condition is satisfied.
- def self.topic(name,&condition)
- @@topics[self] ||= []
- @@topics[self] << Topic.new(name,condition)
- end
- end
Controller
Der Controller stellt die Steuerungseinheit dar und wird über topics von der view über Ereignisse der Benutzeroberfläche informiert.
- # Basic controller class
- class Map_Controller
- # Wrapps a view for init.
- View_Initializer = Struct.new(:klass,:arguments)
- # Wrapps a model for init.
- Model_Initializer = Struct.new(:id,:klass,:arguments)
- # Model reference data structure.
- class Model_Wrapper
- #Ctor.
- def initialize
- @models = []
- end
- # Adds a model instance.
- # param model {Map_Model} The model to add.
- def push(model)
- raise(TypeError, "Must be a Map_Model") unless model.is_a?(Map_Model)
- @models << model
- end
- alias << push
- # Get a model instance by id.
- # param id {Symbol} The model id.
- # return The model with the given id or nil.
- def get_by_id(id)
- @models.detect {|m| m.id == id}
- end
- end
- # View cache.
- @@views = {}
- # Model cache.
- @@models = {}
- # Ctor.
- def initialize
- # Create view if given
- if @@views.keys.include?(self.class)
- initializer = @@views[self.class]
- @view = initializer.klass.new()
- @view.instance_variable_set(:@controller,self)
- end
- # Create models if given
- @models = Model_Wrapper.new
- if @@models.keys.include?(self.class)
- @@models[self.class].each do |model_init|
- m = model_init.klass.new(*model_init.arguments)
- m.id = model_init.id
- @models << m
- end
- end
- # Call view init if view given.
- @view.init(*initializer.arguments) unless @view.nil?
- # Call init.
- init
- end
- # Post construction hook.
- def init
- end
- # Hook for subscribed view topics.
- # param topic {Symbol} The topic name.
- def on_event(topic)
- end
- # Update forwarding.
- def update
- @view.update unless @view.nil?
- end
- # Dispose forwarding.
- def dispose
- @view.dispose unless @view.nil?
- end
- # Static functions.
- class << self
- # Links a view.
- # param value {Symbol} The view class name.
- # param args {Array} The arguments for the view post construction hook.
- def view(value,*args)
- klass = const_get(value)
- unless Map_View == klass.superclass
- raise TypeError, "Must be an instance of Map_View"
- end
- @@views[self] = View_Initializer.new(klass,args)
- end
- # Links a model.
- # param id {Symbol} Id for the model.
- # param value {Symbol} The model class name.
- # params args {Array} The model constructor arguments.
- def model(id,value,*args)
- klass = const_get(value)
- unless Map_Model == klass.superclass
- raise TypeError, "Must be an instance of Map_Model"
- end
- @@models[self] = [] unless @@models.keys.include?(self)
- @@models[self] << Model_Initializer.new(id,klass,args)
- end
- end
- protected
- # View access.
- def view
- @view
- end
- # Model access.
- def models
- @models
- end
- end
Scene_Map Erweiterung
Hier letzten Endes die Erweiterung der Scene. Bei bedaft kann das auch auf andere Scenen übertragen werden.
- # Generic Scene_Map extension API.
- class Scene_Map
- # Data structure for MVC Map extensions.
- class MVC_Wrapper
- include Enumerable
- # Ctor.
- def initialize()
- @units = []
- end
- # Adds a new extension.
- # param value {Symbol} Name of the Controller class.
- def push(value)
- klass = self.class.const_get(value)
- unless Map_Controller == klass.superclass
- raise TypeError, "Must be an instance of Map_Controller"
- end
- @units << klass
- self
- end
- alias << push
- # Enumerable base override.
- def each
- @units.each {|i| yield i}
- end
- end
- # Static constant extension api.
- ADDITIONAL_MVC = MVC_Wrapper.new
- alias map_mvc_main main
- def main
- @addintional_mvc = ADDITIONAL_MVC.map {|controller| controller.new}
- map_mvc_main
- @addintional_mvc.each {|controller| controller.dispose}
- end
- alias map_mvc_update update
- def update
- @addintional_mvc.each {|controller| controller.update}
- map_mvc_update
- end
- end
Anwendungsbeispiel
Das folgende Minimalbeispiel zeigt eine Anwendung, bei der die aktuelle Gesamtanzahl an durchlaufenen Frames auf der Map angezeigt wird. Zudem kann die Anzeige durch betätigen der Pfeiltasten horizontal verschoben werden (Alle 5 Anschläge)
- class Frames < Map_Model
- def current
- Graphics.frame_count.to_s
- end
- end
- class Fives < Map_Model
- model_attr :count, :default => 5, :validate => {
- :presence => true,
- :type => Integer,
- :custom => lambda {|value| value % 5 == 0 }
- }
- def add(val)
- self.count += val
- end
- def sub(val)
- self.count -= val
- end
- end
- class Frames_View < Map_View
- topic(:input_left) {Input.trigger?(Input::LEFT)}
- topic(:input_right) {Input.trigger?(Input::RIGHT)}
- def init
- @vp = Viewport.new(0, 0, 640, 480)
- @vp.z = 99999
- @frames = models.get_by_id(:frames)
- @sprite = Sprite.new(@vp)
- @sprite.bitmap = Bitmap.new(640,36)
- end
- def on_update
- @sprite.bitmap.clear
- @sprite.bitmap.draw_text(16,0,624,36,@frames.current,0)
- @sprite.update
- end
- def left
- counter.sub 1
- if counter.validate
- @sprite.x = counter.count
- end
- end
- def right
- counter.add 1
- if counter.validate
- @sprite.x = counter.count
- end
- end
- def dispose
- @sprite.dispose
- end
- private
- def counter
- models.get_by_id(:counter)
- end
- end
- class Frames_Controller < Map_Controller
- view :Frames_View
- model :frames, :Frames
- model :counter, :Fives
- def init
- view.subscribe(:input_left)
- view.subscribe(:input_right)
- end
- def on_event(topic)
- case(topic)
- when :input_left
- view.left
- return
- when :input_right
- view.right
- return
- end
- end
- end
- Scene_Map::ADDITIONAL_MVC << :Frames_Controller
Dieses Script erlaubt eine gewisse Zeitsteuerung des Spiels. Es gibt synchrone Timer, die mit Hilfe einer FiFo Queue geupdatet werden können und asynchrone Timer, die in einem eigenen Ruby-Thread laufen.
Beide Strukturen stehen als Basisklassen zur Verfügung und können abgeleitet werden.
Grenzen
Jede Form von absoluter Zeit, also "führe zu einem bestimmten Zeitpunkt aus", benötigt Unterstützung des Betriebssystems. In Windows muss man dazu bestimmte Funktions-Pointer übergeben, sprich callbacks definieren. Da jedoch die API des Maker Ruby an der Stelle sehr begrenzt ist, funktioniert nur ein Konzept der relativen Zeit ("Führe in min. n Zeiteinheiten aus")
Funktionsweise
Man leitet eine der beiden Timer-Typen ab und redefiniert die vorgeschriebenen Methoden. Der Timer besteht aus einem Zeitinterval und einer Endbedingung und wird solange mit dem angegebenen Interval als Abstand den Code ausführen, bis die Bedingung erfüllt ist. Dies gilt für beide Typen. Der asynchrone Timer bietet zusätzlich die Auswahl, ob er automatisch oder per Methodenaufruf gestartet werden soll.
Script
Alle weiteren Anweisungen sind den Kommentaren des Scripts zu entnehmen:
- # Extends Integer class for time based values.
- # The pure integer counts as a millisecond value.
- # Example:
- # 1 => 1ms
- # 1.seconds => 1000ms
- # 1.minutes => 60000ms
- # 1.hours => 3600000ms
- class Integer
- # Interpret the value as seconds.
- def seconds
- self * 1000
- end
- # Interpret the value as minutes.
- def minutes
- self.seconds * 60
- end
- # Interpret the value as hours.
- def hours
- self.minutes * 60
- end
- end
- # Extends Float class for time based values.
- # Example:
- # 0.5.seconds => 500ms
- # 0.5.minutes => 30000ms
- # 0.5.hours => 1800000ms
- class Float
- # Interpret the value as seconds.
- def seconds
- frac = self - self.to_i
- full = self - frac
- full.to_i.seconds + (frac * 1000).to_i
- end
- # Interpret the value as minutes.
- def minutes
- frac = self - self.to_i
- full = self - frac
- full.to_i.minutes + (frac * 60000).to_i
- end
- # Interpret the value as hours.
- def hours
- frac = self - self.to_i
- full = self - frac
- full.to_i.hours + (frac * 3600000).to_i
- end
- end
- # Timer module.
- module Timer
- # Updates all sync timers.
- # Use this in your Scene update loop.
- def self.update
- LIST.update
- end
- # Basic timer interface.
- module Base
- # This method is called whenever the time interval matches.
- # You must override this method.
- def exec
- raise NotImplementedError, "You need to implement the exec Method"
- end
- # Timer finished hook.
- # Override this method if you need code to be executed after the timer
- # terminated.
- def on_finished
- end
- # The termination condition.
- # Must return a boolean value.
- def finished?
- true
- end
- end
- # Sync timer base class.
- class Sync
- include Timer::Base
- # Ctor.
- # param rate {Integer} The time interval in milliseconds.
- def initialize(rate=50)
- @rate = rate
- @current = Time.now
- Timer::LIST << self
- end
- # Update process.
- # 'exec' is called if current time matches the interval.
- def update
- if !finished? && (Time.now - @current) * 1000 >= @rate
- exec
- @current = Time.now
- end
- if finished?
- finish
- end
- end
- private
- def finish
- Timer::LIST.delete(self)
- on_finished
- end
- end
- # Async timer base class.
- class Async
- include Timer::Base
- # Error type for restarted timers.
- class StateError < StandardError
- end
- # Ctor.
- # param rate {Integer} The time interval in milliseconds.
- # param auto {Boolean} Set this 'true', if you want to start the timer during construction.
- def initialize(rate=50,auto=false)
- @rate = rate
- @running = false
- run if auto
- end
- # Starts the async timer.
- def run
- raise(StateError,"Timer already started.") if @running
- @running = true
- Thread.new do
- current = Time.now
- until finished?
- if (Time.now - current) * 1000 >= @rate
- begin
- exec
- current = Time.now
- rescue Exception => e
- Thread.main.raise e
- end
- end
- end
- on_finished
- end
- end
- end
- # Sync timer queue data structure.
- class List
- # Ctor.
- def initialize()
- @timers = []
- end
- # Adds a sync timer.
- # param timer {Timer::Sync} The timer to enqueue.
- def push(timer)
- raise(TypeError,"Must be a Timer::Sync") unless timer.is_a?(Timer::Sync)
- @timers << timer
- self
- end
- alias << push
- # Deletes a sync timer.
- # param timer {Timer::Sync} The timer to dequeue.
- def delete(timer)
- raise(TypeError,"Must be a Timer::Sync") unless timer.is_a?(Timer::Sync)
- if @timers.include?(timer)
- @timers.delete(timer)
- end
- self
- end
- # Updates all enqueued timers.
- def update
- @timers.each {|t| t.update}
- end
- end
- # The sync timer queue instance.
- LIST = List.new
- end
Anwendungsbeispiel
Das nachfolgende Beispiel zeigt eine minimale Anwendung des Scripts:
- class My_Sync_Timer < Timer::Sync
- attr_accessor :count
- def initialize
- super(1.seconds)
- @count = 5
- end
- def exec
- @count -= 1
- $game_system.se_play($data_system.decision_se)
- end
- def finished?
- @count == 0
- end
- def on_finished
- $game_system.se_play($data_system.escape_se)
- end
- end
- class My_Async_Timer < Timer::Async
- attr_accessor :count
- def initialize
- super(0.5.seconds,true)
- @count = 8
- end
- def exec
- @count -= 1
- $game_system.se_play($data_system.buzzer_se)
- end
- def finished?
- @count == 0
- end
- def on_finished
- $game_system.se_play($data_system.escape_se)
- end
- end
- class Scene_Timer_Test
- def main
- @sprite = Sprite.new
- @sprite.bitmap = Bitmap.new(640,480)
- @sync = My_Sync_Timer.new
- @async = My_Async_Timer.new
- Graphics.transition
- loop do
- Graphics.update
- Input.update
- Timer.update
- update
- redraw
- if $scene != self
- break
- end
- end
- Graphics.freeze
- @sprite.dispose
- end
- private
- def update
- if @sync.finished? && @async.finished?
- $scene = Scene_Map.new
- return
- end
- end
- def redraw
- @sprite.bitmap.clear
- @sprite.bitmap.draw_text(0,0,640,240,"sync: #{@sync.count}",1)
- @sprite.bitmap.draw_text(0,240,640,240,"async: #{@async.count}",1)
- end
- end
Dieses Script ist eine pathfinding Implementierung, die den A* Algorithmus mit der Manhattan Distanz Funktion als Heuristik nutzt.
Features
- Berechnung des kürzesten Wegs zwischen einer Game_Character Instanz und einem Zielpunkt (Map Koordinaten)
- Direkte Umwandlung in eine RPG::MoveRoute.
- Navigation zu einem Zielpunkt.
- Navigation zu dem nächstgelegenen Feld neben dem Zielpunkt.
- Callback Schnittstelle für die Ausführung von Code nach einer Navigation.
- Touch event Erkennung, wenn der Spieler in einer Route darüber läuft.
- Navigiere einen Spieler neben ein Event.
- Enter-Tasten simulation.
- Navigiere ein Event neben den Spieler.
- Lasse ein Event dem Spieler folgen, bis es neben ihm steht.
A* Implementierung
Nachfolgend das Script für den A* Algorithmus:
- # A* pathfinding class.
- class A_Star
- # Abstract description of a map field.
- class Position < Struct.new(:x,:y,:parent,:cost)
- include Comparable
- def <=>(other)
- if self.x == other.x && self.y == other.y
- return 0
- end
- -1
- end
- end
- # Ctor.
- # param char {Game_Character} The char to calculate the starting position from.
- # param x {Integer} The target x coordinate (Map Coordinate).
- # param y {Integer} The target y coordinate (Map Coordinate).
- def initialize(char,x,y)
- @start = Position.new(char.x,char.y,nil,0)
- @target = Position.new(x,y,nil,0xFFFFFF)
- @open = [@start]
- @closed = []
- @char = char
- end
- # Calculates the path.
- # return {A_Star::Position} The target node if a path was found, else nil.
- def run
- until @open.include?(@target) || @open.empty?
- step
- end
- if @open.empty?
- return nil
- end
- @target.parent = @closed.last
- @target.cost = @closed.last.cost + 10
- @target
- end
- # Converts a path tree to a move route.
- # param node {A_Star::Position} The calculated path target node (see #run).
- # param repeat {Boolean} (optional) Set this 'true' if you want the route to be repeated.
- # param skippable {Boolean} (optional) Set this 'false' if you want the route to block, if the char can't move on.
- # return {RPG::MoveRoute} The converted move route.
- def to_move_route(node,repeat=false,skippable=true)
- route = RPG::MoveRoute.new
- route.repeat = repeat
- route.skippable = skippable
- nodes = node_list(node)
- nodes.pop
- unless nodes.empty?
- route.list.pop
- last = @char
- until nodes.empty?
- current = nodes.pop
- route.list << node_to_command(last,current)
- last = current
- end
- route.list << RPG::MoveCommand.new(0)
- end
- route
- end
- private
- def node_to_command(last,current)
- retval = RPG::MoveCommand.new(0)
- if current.x < last.x
- retval = RPG::MoveCommand.new(2)
- elsif current.y < last.y
- retval = RPG::MoveCommand.new(4)
- elsif current.x > last.x
- retval = RPG::MoveCommand.new(3)
- elsif current.y > last.y
- retval = RPG::MoveCommand.new(1)
- end
- retval
- end
- def node_list(leaf)
- retval = []
- node = leaf
- until node.nil?
- retval << node
- node = node.parent
- end
- retval
- end
- def step
- @open.sort! {|left,right| left.cost <=> right.cost}
- node = @open.shift
- @open += fields(node)
- @closed << node
- end
- def manhattan_distance(position)
- 10 * ((@target.x - position.x).abs + (@target.y - position.y).abs)
- end
- def fields(position)
- retval = []
- field_for(retval,position,4,-1,0)
- field_for(retval,position,8,0,-1)
- field_for(retval,position,6,1,0)
- field_for(retval,position,2,0,1)
- retval.each { |node| node.cost = position.cost + manhattan_distance(node) }
- retval
- end
- def field_for(buffer,position,direction,x_offset,y_offset)
- x = position.x + x_offset
- y = position.y + y_offset
- retval = false
- if $game_map.passable?(x,y,direction)
- node = Position.new(x,y,position)
- unless @closed.include?(node)
- buffer << node
- retval = true
- end
- end
- retval
- end
- end
Game_Character Erweiterungen
Im folgenden Script befinden sich die nötigen shortcut Implementierungen, die sowohl für den Spieler, als auch für events gültig sind:
- # Game_Character extension for short calls.
- class Game_Character
- # Calculates the move route from this char to a given position.
- # param x {Integer} The target x coordinate (Map Coordinate).
- # param y {Integer} The target y coordinate (Map Coordinate).
- # return {RPG::MoveRoute} The calculated move route.
- def find_path(x,y)
- pathfinder = A_Star.new(self,x,y)
- node = pathfinder.run
- pathfinder.to_move_route(node)
- end
- # Calculates the position for the 4 fields around the target and chooses the
- # field for the path with the lowest cost.
- # Can be used to navigate next to an event or any other impassable field.
- # param x {Integer} The target x coordinate (Map Coordinate).
- # param y {Integer} The target y coordinate (Map Coordinate).
- # return {RPG::MoveRoute} The calculated move route.
- def path_next_to(x,y)
- final_nodes = []
- pathfinder = A_Star.new(self,x-1,y)
- node = pathfinder.run
- final_nodes << node unless node.nil?
- pathfinder = A_Star.new(self,x,y-1)
- node = pathfinder.run
- final_nodes << node unless node.nil?
- pathfinder = A_Star.new(self,x+1,y)
- node = pathfinder.run
- final_nodes << node unless node.nil?
- pathfinder = A_Star.new(self,x+1,y)
- node = pathfinder.run
- final_nodes << node unless node.nil?
- node = nil
- unless final_nodes.empty?
- node = final_nodes.sort {|left,right| left.cost <=> right.cost}.shift
- end
- pathfinder.to_move_route(node)
- end
- # Registers a move complete observer.
- # param name {Symbol} The identifier for the observer.
- # param proc {Block} The code to be executed on move complete.
- # block param {Game_Character} The moved char.
- def on_move_complete(name,&proc)
- @move_observers ||= {}
- @move_observers[name.to_sym] = proc
- self
- end
- # Unregisters a move complete observer.
- # param name {Symbol} The identifier for the observer.
- def delete_move_observer(name)
- unless @move_observers.nil?
- @move_observers.delete(name.to_sym)
- end
- self
- end
- # Triggers all move complete observers without touching the list.
- def trigger_move_observers
- unless @move_observers.nil?
- @move_observers.values.each {|proc| proc.call(self)}
- end
- self
- end
- # Triggers and clears all move complete observers.
- def trigger_move_observers!
- trigger_move_observers
- @move_observers = nil
- self
- end
- protected
- # Generic navigation function.
- # param x {Integer} The target x coordinate (Map Coordinate).
- # param y {Integer} The target y coordinate (Map Coordinate).
- # param type {Integer} The navigation type:
- # 0 => move to target
- # 1 => move next to target
- def navigate(x,y,type=0)
- if 0 == type
- route = find_path(x,y)
- elsif 1 == type
- route = path_next_to(x,y)
- else
- raise ArgumentError
- end
- last = route.list.pop
- unless route.list.empty?
- if block_given?
- yield route.list
- end
- end
- route.list << last
- force_move_route(route)
- end
- end
Game_Player Erweiterungen
Die Erweiterungen, die den Spieler betreffen:
- # Game_Player extension for short calls.
- class Game_Player < Game_Character
- # A new attribute to decide if the player resolves touch events while moving.
- # Set this 'true' to activate touch event detection.
- attr_accessor :touch_while_moving
- # Checks a field for a touch event during a move operation and starts it if present.
- # param x {Integer} The target x coordinate (Map Coordinate).
- # param y {Integer} The target y coordinate (Map Coordinate).
- # return {Boolean} True if an event was started, else false.
- def check_event_on_move(x, y)
- result = false
- if $game_system.map_interpreter.running?
- return result
- end
- for event in $game_map.events.values
- if event.x == x and event.y == y and [1,2].include?(event.trigger)
- event.start
- result = true
- end
- end
- return result
- end
- # Move override to use the touch event recognition (see default Game_Character).
- def move_down(turn_enabled = true)
- super(turn_enabled)
- check_event_on_move(@x, @y-1) if touch_while_moving
- end
- # Move override to use the touch event recognition (see default Game_Character).
- def move_left(turn_enabled = true)
- super(turn_enabled)
- check_event_on_move(@x+1, @y) if touch_while_moving
- end
- # Move override to use the touch event recognition (see default Game_Character).
- def move_right(turn_enabled = true)
- super(turn_enabled)
- check_event_on_move(@x-1, @y) if touch_while_moving
- end
- # Move override to use the touch event recognition (see default Game_Character).
- def move_up(turn_enabled = true)
- super(turn_enabled)
- check_event_on_move(@x, @y+1) if touch_while_moving
- end
- # Navigates the player to the given position.
- # param x {Integer} The target x coordinate (Map Coordinate).
- # param y {Integer} The target y coordinate (Map Coordinate).
- # param additional_commands {Array} List of additional move commands.
- def navigate_to(x,y,*additional_commands)
- navigate(x,y,0) do |list|
- list << RPG::MoveCommand.new(45,["$game_player.trigger_move_observers"])
- additional_commands.each {|command| list << command}
- end
- end
- # Navigates the player next to the given position.
- # param x {Integer} The target x coordinate (Map Coordinate).
- # param y {Integer} The target y coordinate (Map Coordinate).
- # param additional_commands {Array} List of additional move commands.
- def navigate_next_to(x,y,*additional_commands)
- navigate(x,y,1) do |list|
- list << RPG::MoveCommand.new(45,["$game_player.trigger_move_observers"])
- additional_commands.each {|command| list << command}
- end
- end
- # Moves the player to a map event.
- # param id {Integer} The event id.
- # param with_trigger {Boolean} (optional) Set this 'true' if you want to trigger
- # the event after moving next to it.
- def move_to_event(id,with_trigger=false)
- event = $game_map.events[id]
- if event
- additional = []
- if with_trigger
- additional << RPG::MoveCommand.new(45,["$game_player.press_trigger"])
- end
- navigate_next_to(event.x,event.y,*additional)
- end
- end
- # Simulates the press of the return key.
- def press_trigger
- kb = Win32API.new("user32.dll", "keybd_event", "nnnn", "v")
- kb.call(0x0D, 0, 0, 0)
- Input.update
- kb.call(0x0D, 0, 2, 0)
- end
- end
Game_Event_Erweiterungen
Die Erweiterungen, die Events betreffen:
- # Game_Event extension for short calls.
- class Game_Event < Game_Character
- # Navigates the event to the given position.
- # param x {Integer} The target x coordinate (Map Coordinate).
- # param y {Integer} The target y coordinate (Map Coordinate).
- # param additional_commands {Array} List of additional move commands.
- def navigate_to(x,y,*additional_commands)
- event_navigation(x,y,0,*additional_commands)
- end
- # Navigates the event next to the given position.
- # param x {Integer} The target x coordinate (Map Coordinate).
- # param y {Integer} The target y coordinate (Map Coordinate).
- # param additional_commands {Array} List of additional move commands.
- def navigate_next_to(x,y,*additional_commands)
- event_navigation(x,y,1,*additional_commands)
- end
- # Moves the event next to the player.
- # param with_trigger {Boolean} (optional) Set this 'true' if you want to trigger
- # the event after moving next to it.
- # The trigger is skipped, if the event is not next to the player after moving.
- def move_to_player(with_trigger=false)
- additional = []
- if with_trigger
- command = "$game_map.events[#{self.id}].start "
- command += "if Game_Event.next_to_player?(#{self.id})"
- additional << RPG::MoveCommand.new(45,[command])
- end
- navigate_next_to($game_player.x,$game_player.y,*additional)
- end
- # Moves to player and checks if the event is next to the player.
- # If not, it countinues to follow the player with another move route.
- # param whith_trigger (see #move_to_player)
- def follow_player(with_trigger=false)
- id = self.id
- name = "follow_player_observer_#{(Time.now.to_f * 1000).to_i}".to_sym
- proc = Proc.new do |event|
- unless Game_Event.next_to_player?(id)
- follow_player(with_trigger)
- end
- delete_move_observer(name)
- end
- on_move_complete(name,&proc)
- move_to_player(with_trigger)
- end
- # Checks if a given event is next to the player.
- # param id {Integer} The Game_Event id.
- # return {Boolean} True if the the event is next to the player.
- def self.next_to_player?(id)
- event = $game_map.events[id]
- 1 == ($game_player.x - event.x).abs + ($game_player.y - event.y).abs
- end
- private
- def event_navigation(x,y,type,*additional_commands)
- navigate(x,y,type) do |list|
- list.unshift(RPG::MoveCommand.new(37))
- list << RPG::MoveCommand.new(25)
- list << RPG::MoveCommand.new(38)
- list << RPG::MoveCommand.new(45,
- ["$game_map.events[#{self.id}].trigger_move_observers"]
- )
- additional_commands.each {|command| list << command}
- end
- end
- end
Anwendungsbeispiele
Im Folgenden werden einige Befehle beschrieben, die Zeigen sollen, wie das Script funktioniert:
Befehl | Beschreibung |
$game_player.touch_while_moving = true | Schaltet die Touch Event Erkennung ein. |
$game_player.move_to_event(1,true) | Bewegt den Spieler zu einem Event und löst es nach dem Bewegen aus. |
$game_player.find_path(0,0) | Gibt eine RPG::MoveRoute zurück, die den Spieler zu den Map Koordinaten (0,0) navigiert. |
$game_player.navigate_to(0,0) | Navigiert den Spieler zum Punkt (0,0). |
$game_player.navigate_next_to(0,0) | Navigiert den Spieler an den nähesten Punkt neben (0,0). |
$game_map.events[1].move_to_player | Navigiert das Event mit der id 1 neben die aktuelle Position des Spielers. |
$game_map.events[1].follow_player | Das Event mit der id 1 folgt dem Spieler, bis es neben ihm steht (berücksichtigt die Repositionierung des Spielers). |
$game_player.on_move_complete(:my_task) { |player| p "foo" } | Tue wenn der Spieler eine Navigation abgeschlossen hat. |
$game_player.delete_move_observer(:my_task) | Macht die Zeile drüber rückgängig. |
Die Beschriebenen Befehle sind lediglich einige ausgewählte Anwendungsbeispiele und decken nicht alle Möglichkeiten des Scripts ab. Für weitere Hinweise bitte die Kommentare im Script beachten.