Matharoo 又出新教程了,是一套回合制的 RPG 教程,按他自己的说法这套教程还没达到他最初设想的完成度,但因为他现在给官方写教程和文档的占用不少精力,没时间继续于是准备弃坑了,因此直接放出了完整工程、文档及不那么完整的视频教程,其实项目完成度已经非常可观了。
项目视频:「GameMaker RPG Tutorial」
项目工程:奶牛快传 或 github
项目文档:GMS2.3 RPG: Documentation
Google Docs访问可能不太方便,我贴一份原文
Managers
- oGlobalManager, persistently present in all rooms, created in rmInit
- oGameManager, present in the game rooms
- oCameraManager, also present in the game rooms
- oTextboxManager, created when a textbox is needed
- This handles spawning and destroying the accompanying Sequence (for displaying the textbox)
Resolution Handling
**The script scrResolution handles setting up the game’s resolution and window size.
The game is full resolution (res * scale), as well as the GUI layer. All GUI Sequences must be created using the final size (which currently is 1280×720).
The data from this script is used by oCameraManager to create a camera whenever a game room begins.
Input Manager
Global input variables are set in the Begin Step event of oGlobalManager.
State System
The Entity object handles states.
The Player object only gives input.
Each state is now a struct, with left/right/up/down sprites for each direction.
States are stored in a struct called ‘states’.
states = {
idle : {
left: sPlayer\_Left\_Idle,
right: sPlayer\_Right\_Idle,
down: sPlayer\_Down\_Idle,
up: sPlayer\_Up\_Idle
},
walk : {
left: sPlayer\_Left\_Walk,
right: sPlayer\_Right\_Walk,
down: sPlayer\_Down\_Walk,
up: sPlayer\_Up\_Walk
}
}
In the Step event, the movement direction is normalized and rounded into a 0-3 range (to be more safe and avoid any floating point errors).
That is used to set the sprite based on the current state:
if (moveDirection == 0) sprite_index = state.right;
else if (moveDirection == 1) sprite_index = state.up;
else if (moveDirection == 2) sprite_index = state.left;
else if (moveDirection == 3) sprite_index = state.down;
Grid-Based Movement (& Collisions)
These are the variables used:
These are the functions used:
- collision_at_cell
- state_set
- to_room_coords
- to_cell_coords
The following happens during the idle+walk states:
- If there is input while the player is not moving, we set moving to true, calculate the position of the new cell where we’re moving, convert that to room coordinates and apply it to targetX/Y
- This part includes a collision check at the new cell, and only sets movement if there are no collisions
- If there is a collision, the movingDirection is set so that the player faces in the new direction
- The actual movement happens when moving is true. On each axis, movement is applied if there is still some distance to the target, otherwise the position is set to the target.
- If the entity has reached the target position, moving is set to false
NPC System
oNPCParent is the parent for all NPCs.
It is a child of oAIParent.
NPC AI
AI is controlled by oAIParent.
It’s based on MP Grids and Paths.
oGameManager sets up the global AI MP grid, which registers static collision instances and tiles.
oAIParent makes use of this MP grid for its AI.
Whenever its move timer hits, it selects a random point within its movement circle, and calls the moveToPoint() function on that point.
A variable stores which point it’s following within the path, it gets its position and sets the inputX/Y variables accordingly. This way, the entity moves.
In the End Step event it moves on to the next point when it reaches the current one, and also ends the path.
NPC Collisions
Collisions are simply handled by the entity grid movement code.
Textbox Design
The textbox used in the game’s dialogue system is designed in a Sequence.
The sequence is created in a global layer. That global layer is drawn to a global surface, using layer scripts.
That surface can be drawn whenever the active GUI Sequence element needs to be visible.
Textbox Manager
The textbox manager is created by the game manager.
It’s stored as a singleton in a global variable.
The CreateDialogue() method is used to start a conversation.
Dialogue Storage
Dialogue data is stored in a script, using structs.
Each struct contains the speaker and the message.
Speakers are set up with structs as well, within the same script.
Pausing
A global.paused variable controls whether the entity Step code runs.
The functions game_pause() and game_unpause() can be used to pause/unpause the game.
Dialogue System
In the End Step event of oPlayer, it checks the tile where the player is facing, for any NPCs.
If one is found, the speech bubble icon is enabled for it.
If space is pressed, the CreateDialogue() method is run in the textbox manager, reading message data from the NPC.
Entity Stats
Set up in the Variable Definitions window of oEntityParent.
Methods are set up in the Entity object for getting these values.
Stats HUD
Sequence seqPause is created when the game is paused.
The object oPlayerStatsBox is created with it, which draws the player’s stats.
World Enemies
oEnemyParent is used for enemy entities, which is a child of oAIParent.
Turn Based Battle System
Turn-Based Battle Document
Leveling System
Entities have a level and an xp variable.
There are two more variables: nextLevelXP and baseLevelXP.
The former stores the XP required for the next level, returned using the function xp_for_next_level().
The latter stores the XP that was required to get the current level.
Leveling up is handled by the Entity, in the End Step event.
Level and XP info is shown in the pause menu, and during battle.
Collecting XP During Battle
The xpFinal variable in the Battle Manager starts at the player’s XP.
Whenever the player performs an action, XP is added to the xpFinal variable, depending on the selected opponent’s level and some randomness.
When the battle ends, xpFinal is applied to the player’s XP, using an alarm (so that you can see it go up).
It also tests if the player has leveled up (by checking if the new XP is >= than xp_for_next_level()). If it has, level_up_stats() is called, which increases player stats and prints related text to the message box.
Room Changing
oRoomChanger is the trigger that changes the room; it stores the room to go to, and the spawner to go to.
The Room Transition Manager (oRoomTransition) is created when you step on a room changer.
It gets the target room and target spawner from the oRoomChanger.
This object is persistent. It shows a Sequence before ending the current room, and a new Sequence after starting the next room.
Persistent Data b/w Rooms
On Room End, the Room Transition manager saves the player’s properties into a struct, getting them using the get_entity_data() function.
On Room Start, data from that same struct is applied to the player, using the set_entity_data() function.
Item Data
An enum stores all item types.
Item data is set up using constructors, where an array stores a struct for each item type, where the struct is created using the appropriate constructor.
The base constructor is Item, and all other constructor types inherit from it.
Examples: HealingItem, AttackIncrease, DefenseIncrease.
An Item-derived constructor should have an Effect() function, which is called when a player uses that item, and defines what happens to that player on using the item.
A variable called ‘forBattleUse’ defines whether an item is to be used within battle only (true), or only outside of battle (false).
Inventory
Each entity has an array called ‘inventory’, whose size is equal to INVENTORY_MAX_SIZE.
The inventory_add() function is used to add an item to an entity’s inventory.
Each filled slot in the array is a struct created from the Slot constructor.
Inventory GUI
oPlayerInventoryBox draws the contents of the player’s inventory, and allows you to use any items that are compatible with the current state.
Compatibility ensures that if an item is forBattleUse, it can only be used within battle; any other items can be used at all times.
This inventory box is placed in the Pause sequence, so it can be accessed through the pause menu. The Pause sequence is also created during battle, so that the player can look at its stats and use an item.
For more info on the usage of items during battle, refer to the Turn-Based Battle Document.