Jump to content

Player Stats/Skills & Level up Codes


Defranco
 Share

Recommended Posts

I thought I would both share what we've been working, as well as get some feedback on a really complicated line of code for stats/skills and stuff for our upcoming RPG.

 

This is just 0.5% of our code, our original code has dozens of skill-types and thousands of lines of coding.

 

We've now split up our lua script into multiple scripts using GetChild(#) and GetParent() references instead of having it in the FPSPlayer.lua area. It was just getting too long and complicated.

 

What I'm sharing is some of our stats & skill system (archery skill displayed)

 

playerStatsSystem.lua

-- At the top of our lua script.

-- Starting Stats -----------------------------------------
-- Strength: Archery-3:1, OneHanded-1:1, TwoHanded-1:2, Shield-3:1, Defense-3:1
Script.playerStrength = 5 --float "Strength"
-- Dexterity: Archery-1:2, OneHanded-1:1, TwoHanded-5:1, Defense-5:1, Sneak-2:1, Pickpocket-2:1, Lockpick-2:1
Script.playerDexterity = 5 --float "Dexterity"
-- Constitution: Shield-1:2, Defense-1:2, HealingMagic-3:1, BuffMagic-3:1
Script.playerConstitution = 5 --float "Constitution"
-- Intelligence: DamagicMagic-1:2, HealingMagic-2:1, BuffMagic-1:1, Speech-4:1, Vision-4:1, LockPick-3:1
Script.playerIntelligence = 5 --float "Intelligence"
-- Wisdom: DamagicMagic-2:1, HealingMagic-1:1, BuffMagic-1:2, Speech-2:1, Vision-1:1,
Script.playerWisdom = 5 --float "Wisdom"
-- Charisma: Speech-1:2, Lockpick-3:1, Pickpocket-1:1
Script.playerCharisma = 5 --float "Charisma"
-- Luck: Vision-1:1, Lockpick-1:1, Sneak-1:2, Pickpocket-1:1
Script.playerLuck = 5 --float "Luck"

-- Available Starting Unallocated Stat Points --
Script.playerUnspentStatPoints = 9 --float "Unspent Stat Points"

-- Unallocated Stat Points Per Level --
Script.playerLevelUpStatPoints = 1

 

This top part is just the start (the base and foundation) of our stats & skills system. We've set out to do a RPG that is a cross between Ultima Underworld (series) & Arx Fatalis, with many of Skyrims RPG elements.

 

- A player has a starting of 5 points in each Stat when creating a character. He/she can then use the 9 unspent starting stats points to customize the rest of the stats. And the player gets 1 stat point per level-up to use.

 

- Each of these stats increase various skills. For example. Every 1 point in Dexterity gives the player 2 skill points in Archery, and every 3 points in Strength gives the player 1 skill point in Archery.

 

- Stats also increase other values such as health, health regen, mana, stamina, crit chance, dodge chance, etc etc

 

Here is some sample code taken for Archery that continues our script.

 

-- Available Skills --
Script.playerSkillArchery = 0 --damage with bows / crossbows


-- Skills Starting --
Script.playerSkillStartArchery = 0


-- Skills Leveled Up Based on Stats --
--Archery--
Script.playerArcheryValueStrength1 = 3
Script.playerArcheryValueStrength2 = 1
Script.playerArcheryValueDexterity1 = 1
Script.playerArcheryValueDexterity2 = 2
Script.playerArcheryValueEquipment = 0
Script.playerArcheryValueMagic = 0
Script.playerArcheryValueScrolls = 0
Script.playerArcheryValuePotions = 0

-- Available Starting Unallocated Skill Points --
Script.playerUnspentSkillPoints = 22 --float "Unspent Skill Points"

-- Unallocated Skill Points Per Level --
Script.playerLevelUpSkillPoints = 4

 

The above code sets the base for anything that could give the player any Archery points.

 

Stats such as strength and dexterity give the player archery points. But also, equipment, magic spells, scrolls, and potions. These values are at 0 now, but when a player equips a piece of armor that has an archery value on it, the formula will add it to the total archery point.

 

Also, a player gets 22 unspent skill points at character creation. So not only can they put 9 stat points, they can put in 22 skill points to further customize the character. The player also gets 4 extra skill points per level-up.

 

Here is what we have in our Function Script:Start()

------ SETUP ALL SKILLS ------
--Setup Archery--
self.playerSkillArchery = self.playerSkillStartArchery + (self.playerStrength/self.playerArcheryValueStrength1)*self.playerArcheryValueStrength2 + (self.playerDexterity/self.playerArcheryValueDexterity1)*self.playerArcheryValueDexterity2 + self.playerArcheryValueEquipment + self.playerArcheryValueMagic + self.playerArcheryValueScrolls + self.playerArcheryValuePotions

 

What this code does is adds up all the values that gives the player Archery Skills, and it works perfectly.

 

We've also put that same code above into our UpdateWorld. This allows the game to constantly make changes if a player removes/puts on armor, or drinks a potion the values get updated right away.

 

What we still need to do:

-Make a ROUND-DOWN feature. Because as of right now it shows Archery Skill as 11.666666666~ when we want it to just show as 11.

 

-------------------- Here's my question to anyone who read this.

Is there an easier way, maybe more simplified?

Do you have any suggestions / feedback?

 

-- we have dozens of skills, and dozens of attributes such as crit chances with various weapons and tons and tons of perks, right now all these change in the game depends on stats/skills and equipment worn, and potions/scrolls used or magic buffed used. And we're using all these calculations in the UpdateWorld, should we be putting them there too, or is there another way.

  • Upvote 2
Link to comment
Share on other sites

My suggestions:

 

Look into making "classes" in Lua vs using all Le entity scripts for your code. It sounds like you're attaching pivots to the player and adding LE entity scripts to them. That's a pretty hacky way to go about it. You can simulate classes in Lua and then include those into your main player script. I suggest each systemyour play has would have its own class and just include that into the player script. This would easier as the flow of your code is all in one spot.

 

I would also suggest breaking that huge long formula up into multiple lines and label each step. It'll make it easier to change 3 months from now.

 

When I played around with org stuff I would make a stat class. Then make 2 instances of that in my actor class. One is base stats the other modified stats.

 

Maybe lean more towards event programming vs polling with the formulas. If you have a lot and they get complex not having to run them each frame could help. You really only need to calculate when something changes. Knowing when things change between systems means you need to know when things change. That's where an event system would come in handy. I use one that allows you to subscribe any number of function to be called when you fire the event. That can come in handy as it helps move logic to generaaller functions vs polling which tends to lead to giant functions that are complicated to follow. It also helps keep systems less dependent on each other.

 

You asked for comments so thought I'd share. I know I hate it when nobody replies.

Link to comment
Share on other sites

Hey Rick,

 

Greatly appreciate the feedback. Especially anything criticism, as that's how we're going to learn the fastest.

 

We are indeed using multiple pivots: we've got the primary character controller, then we have the hit-box pivot, then the stat/skills pivot, then attached to the stats/skills pivot we have every single bar pivot: health bar, mana bar, stamina bar, etc

 

Event programming is something we are indeed lacking. I guess that when we create the scripts where the player puts a piece of armor on, or removes a piece of armor, or drinks a potions, or potion affects wear off, we would call to the function in those scripts that calls for updating player stats instead of keeping them in update world?

Link to comment
Share on other sites

Here is my event script that I use:

 

if EventManager ~= nil then return end

EventManager = {}

function EventManager:Create(owner)
  local obj = {}

  obj.handlers = {}
  obj.owner = owner

  for k, v in pairs(EventManager) do
     obj[k] = v
  end

  return obj
end

function EventManager:Subscribe(owner, method)
  table.insert(self.handlers, { owner = owner, method = method })
end

function EventManager:Raise(args)
  for i = 1, #self.handlers do
     self.handlers[i].method(self.handlers[i].owner, self.owner, args)
  end
end

 

You would create an event like (Inside InventoryScript for example):

 

self.onDrinkHealingPotion = EventManager:Create(self)

 

 

You would subscribe to it like:

 

InventoryScript.onDrinkHealingPotion:Subscribe(HealthScript, HealthScript.Heal)

 

 

So you can kind of see how we are communicating between scripts. Inside the Inventory script we have the onDrinkHealingPotion event and when it's raised it'll call the HealthScript's Heal function so the health information can be updated.

 

 

I've been changing our player code for our game to more of a component design. It's similar to what you're doing except I don't use pivots/entities. I have the 1 entity which is the player, and then inside that script I bring in all the other scripts/components that make up the player and hook all the functions together so they can interact. This is nice because it makes it so each component is not coupled with other components. You can think of each component like a library in itself doing it's specific thing. All it needs is functions that act on the state of its own variables and events to tell the outside world things it may need to know.

 

 

The basic structure of a component script is:

 



if PlayerSound ~= nil then return end

PlayerSound = {}

function PlayerSound:Init(entity)
  self.entity = entity
end

function PlayerSound:Update()
end

 

Then inside my player entity script I'd bring it in and hook it up:

 

import "Scripts/Objects/Components/PlayerSound.lua"
import "Scripts/Objects/Components/PlayerInput.lua"
function Script:Start()
  PlayerInput:Init(self.entity)
  PlayerSound:Init(self.entity)

  PlayerInput.onmoveForward:Subscribe(PlayerSound, PlayerSound.MoveForward)
end

 

So the main player script just ends up being a place where each different component comes together and hooks up events to give the functionality that makes up the player. I've had to switch to this component design because sooner or later the player script just gets too big and unmanageable so splitting up the functionality by domain helps keep things organized better and in their own little scripts doing their 1 specific thing and communicating via events. Everything a script needs to know about some other script is done via events and events can have args so it gets it's information from the other script that way.

 

If you need you can create an onGetRotation event which you would fire off in that scripts Update. I do this between the PlayerCamera and PlayerController as the controller needs the Y rotation of the camera so I have the PlayerCamera onGetRotation event pass that as a param:

 

function PlayerCamera:Update()
  -- do camera rotation stuff here

  self.onGetRotation:Raise({ camRot = self.camRotation })
end

 

I'll subscribe to that to my PlayerController and now each frame my PlayerController knows the cameras rotation that it can pass to the SetInput() function.

 

Sorry for the long post.

  • Upvote 1
Link to comment
Share on other sites

three hurrays for component based design.

 

It can be challenging mixing it into LE since LE isn't component based. Some things don't make total sense. Like my PlayerController component. The LE entity is the thing that can be a controller so inside PlayerController I make it a character controller with SetPhysicsMode() and then in it's update I do SetInput(). Normally this component itself would be a controller but I'm working with how LE works and it still works out.

 

If a person isn't used to communicating between components with events I can see how it might seem strange and not needed (complex) but I find it helps me ask the question for each piece of functionality:

 

What component needs to know about this event. Then I wire up the event to said components so they can handle it. It can be intimidating to see a bunch of events being hooked up to functions, but it's actually really easy to read when it take it one at a time. You understand that this component is listening to this event and does this function when that event is raised. Much easier than a monolithic player class with thousands of lines of code with all sorts of domains mixed in and state if statements all over the place. That was getting to be a disaster to read and maintain.

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

 Share

×
×
  • Create New...