Action RPG Framework

by Forum_account
Action RPG Framework
A framework for developing action RPGs.
ID:806024
 
Most of the changes in this update were to the sample game. I moved the enemy AI code from the sample game to the library. Now all mobs have default AI (note: the AI proc is only executed for mobs without clients). If you want to change the AI you can override the mob's AI proc. By default mobs will wander (the wandering distance is limited by their wander_distance var, a value of zero means they don't wander), look for hostile targets, and approach and attack their target. They use their team var to determine which mobs are hostile, so by putting NPCs on the player's team and enemies on the enemy's team, you can create guards that attack enemies without having to change their ai() proc (there's an example of this in the sample game, see demo\npcs.dm).

The mob's team var is a set of bit flags. This lets you create different factions. Mobs A and B are hostile towards each other if A.team & B.team is zero (they have no common team bits set). If two mobs have just one team in common they won't attack each other. This lets you easily do things like this:

Constants
var
const
TEAM_GOBLINS = 4

mob
goblins
team = Constants.TEAM_GOBLINS

This makes goblins hostile to players and enemies. When the player completes a quest to win the favor of the goblins, you can set the player's team var to Constants.TEAM_PLAYERS | Constants.TEAM_GOBLINS and the goblin mobs won't attack them anymore (but will still attack enemies).

I also removed the global Combat var from the library. You have to define your own in your project. I also made the sample game define two types of Combat objects - PhysicalCombat and MagicCombat - which are used for dealing physical damage and magical damage. Physical attacks can be dodged, magical attacks can be resisted. It's similar but they use different stats. I also updated each ability in the sample game to use either of those combat objects.

I also updated the procs for playing sound effects and added sound effects to the sample game.

Here is the full list of changes:
  • Fixed a movement bug. Previously you would continue moving if you held and arrow key while you died.
  • Moved the enemy AI into the library to be the default ai() proc. Non-client mobs that don't override their ai() proc will wander, look for targets, and use their abilities to attack them.
  • Added some vars to manage the default enemy AI. Check enemy-ai.dm for more details.
  • Made the font of the chat input field match the rest of the interface.
  • Removed the global Combat var. If you need to use it, define your own.
  • Defined the PhysicalCombat object in the sample game which contains the rules for physical damage (dodging, critical hits, etc.).
  • Added the MagicCombat object to the sample game which has slightly different rules than the PhysicalCombat object. It uses the mind stat for critical hits and the resistance stat for damage reduction.
  • Added the mind and resistance stats which are used by the new MagicCombat object.
  • Changed how teams work. The team var is now a bit mask. If two mobs have no matching bits set, they can attack each other. By default all mobs are on TEAM_PLAYERS and mobs of type /mob/enemy are only on the TEAM_ENEMIES team.
  • Removed the TARGET_RANGE constant. If you want to change it, just override the default value of the mob's target_range var.
  • Added the mob.ignore and unignore procs which can be used to ignore chat messages from certain players. Your ignore list stores each mob's ckey, but you can pass a ckey or mob reference to these procs.
  • Made the player able to target non-enemies by holding down the shift key while pressing tab.
  • Also added the ability to target a mob by clicking on their name in the chat log.
  • Added the name display below targeted mobs.
  • Made projectiles pass through friendly mobs.
  • Updated the sound procs. There is now the atom.noise() proc and the mob.sound_effect() proc. The atom.noise() proc is for creating noises that nearby players can hear (ex: casting a spell). The mob.sound_effect() proc is for playing sound effects to a single player (ex: when they make a selection in a menu).
  • Added sound effects to the sample game.
  • Added the mob.rumble proc which makes the screen shake.
I also added screenshots to the library's hub entry. The screenshots are of the sample game and most of its features are provided by the library: http://www.byond.com/developer/Forum_account/ ActionRpgFramework?tab=pics
*stands and claps* brovo sir
In response to PixelRat
PixelRat wrote:
*stands and claps* brovo sir

Thanks!

And, as always, if there's something anyone would like to see added to this, just post on the forum. Here's the list of what I'm planning to add in the next update:

 * selling items
x equipment overlays (just need to bother making the graphics)
x I can make them for body armor and helmets, those are easiest.
- If I have the time, I'd like to make a sword and shield
* more elaborate on-screen target info display
- would have buttons for ignoring, private messaging, inviting to party, etc.
* chat menu for unignoring players
* add mouse input
- I'm not sure if I'd want to bother adding this, I like it being keyboard-only
x change how attack animations work
x use a flag and change the icon_state in set_state()
* expand the sample game to include more equipment slots
If you need any icons just ask i love to contribute to this
i agree that it should be keyboard only. that way you could have some option for each letter of the keyboard. you could make the framework very advanced that way. besides, its a bit cumbersome moving from keyboard to mouse all the time.

overlays would be a good idea. you could make one player where you could change the head or body of that player. that way, everyone would look different in game.
Forum_account wrote:
> Constants
> var
> const
> TEAM_GOBLINS = 4
>
> mob
> goblins
> team = Constants.TEAM_GOBLINS
>


This code doesn't compile. Though, it would be quite awesome if DM actually let you do something like this.

EDIT: Maybe it does work in your library. If it does, it'd be awesome if you explained how you made it possible.
var
Constants/Constants = new()


Pretty standard way to simulate a static set of constants, I thought. I use it in:

http://www.byond.com/developer/Stephen001/UUID

var/UUID/new_id = UUID.generate(UUID.VERSION_4)


And if I recall, I've seen it used elsewhere also.
In response to Stephen001
Oh, I see. It seems like this should be built-in rather than having to instantiate an object. (Like static class vars in C++.)
Makes no difference really, having it built-in is just syntactic sugar for the most part, given you won't have a memory requirement in DM that actually means you'd have to care that it's an instantiated global versus static referencing.
Yes, that's correct. As long as the vars belonging to the Constants object are defined as const, you can reference them in situations where a constant is required.

I like it better than using global vars or #define statements to make constants. I often use global objects like this to give procs or vars a "namespace". It's not terribly useful but when people see "Constants.TEAM_GOBLIN" in the code, they know where the TEAM_GOBLIN var is defined (they know it's not a global var or member of the src object).

The only problem I've had is that this doesn't work:

client
view = Constants.VIEW_WIDTH + "x" + Constants.VIEW_HEIGHT

You can concatenate constant strings, but I guess since the width and height are numbers the expression is not recognized as a constant.
I don't think there's an easy or clean way to have static objects in DM. By having to put this to create the global instance:

var
Constants/Constants = new()

I'm specifying that the global instance is called "Constants". If you had:
mob
Constants
static/const/SOMETHING = 3

How would you reference the "SOMETHING" var? You couldn't simply say "Constants.SOMETHING". It's possible to have other object types called Constants (ex: /obj/Constants) so you'd have to specify the full path. There's no "import" type of statement in DM that lets you say "when I say 'Constants', I'm really referring to /mob/Constants".

Also, by naming the namespace as a global var you can easily switch what object is being used without having to change how constants are referenced:

Constants
var/const/SOMETHING = 2

MyConstants
var/const/SOMETHING = 5

var
// Constants/Constants = new()
MyConstants/Constants = new()
In response to Forum_account
Forum_account wrote:
> mob
> Constants
> static/const/SOMETHING = 3
>

How would you reference the "SOMETHING" var? You couldn't simply say "Constants.SOMETHING". It's possible to have other object types called Constants (ex: /obj/Constants) so you'd have to specify the full path. There's no "import" type of statement in DM that lets you say "when I say 'Constants', I'm really referring to /mob/Constants".

I'm thinking about requesting something like this as a feature. But, essentially it would be nice to be able to do something like so:
thing
var
love = 10

client
verb
Test()
src << "thing loves you ×[/thing/var/love]"

But the syntax quickly becomes ugly. The reason I even care about having an object always instantiated is if I wanted to have many different objects that define serialized data. For one, you can't modify inherited const vars.
thing
var/const
love = 10
something
love = 3 //this is a no-go

If you had many many classes with constants defined, you would have to make many instances of them, potentially using up a lot of memory.
And the problem is pushed further when you try to define constants on an actual class whose main purpose isn't constants. Because, the New() proc will be called, and all of its unused data will be in memory just sitting there.
Since the data is already in the DMB itself, it also doesn't make sense to have another instance of it just to access that data. That's another reason I think it should be exposed. In any case, I probably will make a feature request when I can find the best way to explain and request it.
The thing is, that's a rubbish amount of memory, even with a lot of constant types. You'd need to be proposing a developer has thousands of separate constant types before the argument about memory will hold, and I think the developer is going to be hitting other slightly more fundamental issues before they manage to have thousands of constant types.

The sub-typing aspect isn't really an issue, because you can't scope in DM like you do with other languages. In your syntax example above, you reference /thing/var/love, and so sub-typing like that doesn't afford you any benefits in a static context regardless.

Constants, similarly can be made global, to address your last memory concern. As it cannot be changed, making it var/const/global means it's unmodifiable, and shared between types. This alleviates the memory concern (although I feel the memory concern is most definitely exaggerated here).
In response to Stephen001
Well, it's not at all exaggerated. Say you had an involved RPG with about 600 creature types, each one having around 50 constants that is unique to that creature. (e.g. Pokémon.) Say, each constant is 4 bytes, this is 120,000 bytes of memory. Now, you have an instance of these as well (in the case you needed inheritance to make your syntax actually be useful) you have now doubled that to 240,000 bytes of memory always open. This is estimating low, because many of these constants vary in size from text strings to lists of 50 items. This memory usage builds fast. Making the constants global, doesn't allow you to inherit and redefine them, or allow you to just add constants to any object that isn't used only for constants. I think you're right about it helping in the case of objects used solely as constant classes, though.


Now, I was searching the forums to see if I could find anything related to this, and I found it is actually very possible already to have static procs.
thing
proc
love()
return 10
something
love()
return 3

client
verb
Test()
src << "thing loves you ×[call(/thing/proc/love)()]"

In this example, you can call an object's proc without instantiating that object. As long as you didn't use src in the proc, this would make it viable for use statically.
This syntax is not ideal for defining constants, but, it is manageable. (In this example, '/thing/proc/love' could be replaced with '/thing:love' using advantage of the : path operator.)

EDIT: So what I'd be wanting is something like call() but for vars instead.
An object instance won't take up much memory if you only create it to access its vars. BYOND only uses memory to store the values that deviate from the compile-time values. Turfs have many vars (icon, icon_state, x, y, z, layer, dir, etc.). If you figure out how many bytes it is for all of those vars and multiply it by the number of turfs on the map, you'll be way over the amount of memory it actually uses.

Complex Robot wrote:
But the syntax quickly becomes ugly. The reason I even care about having an object always instantiated is if I wanted to have many different objects that define serialized data. For one, you can't modify inherited const vars.

Remember that they only have to be constants if you're using them to initialize things at compile-time. In your Test() verb you're generating a string at runtime, so you don't need that variable to be a constant.
In response to Complex Robot
Complex Robot wrote:
Say you had an involved RPG with about 600 creature types, each one having around 50 constants that is unique to that creature. (e.g. Pokémon.) Say, each constant is 4 bytes, this is 120,000 bytes of memory. Now, you have an instance of these as well (in the case you needed inheritance to make your syntax actually be useful) you have now doubled that to 240,000 bytes of memory always open.

1. The memory usage wouldn't double like that.

2. 120,000 bytes is 0.114 megabytes. Computers can handle that =)

You're best off using whatever has the nicest syntax here since the difference in performance and resource usage is irrelevant.
In response to Forum_account
I tried making some namespaces in my ProcLib collection to separate things based on function. Unfortunately, some timing issues occurred where a proc was called before the global instance was created. I eventually replaced the namespaces with prefixes in the proc name, separated by an underscore.

I don't think I like how I can't change any of the constants without going into the library and changing it. If I want something completely customizable, I guess I'd have to copy/paste the library into my project where I can change things without changing everything else using the framework.
In response to Forum_account
Are you sure that optimization isn't used solely for turfs? Turfs are a special type of object in DM.

And I think there are some cases when they can't do that.
object
var/global/number = 0
New()
++number

turf
var
list/objs = newlist(/object, /object, /object)


client
verb
Test()
var/object/O = new
src << O.number // Presumably this would be 3 * the number of turfs

EDIT: I tested this with a map of 10×10, and it returned 1. I am missing something.
EDIT2: No, it seems the New proc is not being called at all before the verb is called. You must be right. There is some undocumented feature happening there.
EDIT3: My guess is objects put into compile-time lists never have their New proc called. Which means I've learned something today.
Even if we assume they don't do that, at all, you're talking less than 1 MB of consumption on the server for your example, and as they are globally used instances, it isn't likely to grow. It's just not a concern.
Page: 1 2