Arcane University: Scripting Best Practices
The Beyond Skyrim Wiki — Hosted by UESP
This is a guide of best practices to follow when writing Papyrus scripts.
Papyrus Scripting Best Practices
- COMMENT YOUR CODE! Use
;
for general comments,;/ ... /;
for comment blocks, and{ ... }
for docstrings. - To get started with Papyrus scripting, read through the concepts and tutorials here: https://ck.uesp.net/wiki/Category:Papyrus
- If you expect to do extensive scripting, an external editor & compiler is highly recommended (see above link for instructions for different editors).
- Keep these pages bookmarked; you will use them often:
- Enable and check your Papyrus logs. Make liberal use if
debug.trace(...)
when first writing and testing your code. - Avoid using
Game.GetPlayer()
when possible. It is almost always better to use and autofill an Actor property PlayerREF instead. - If you want to remove a property from a script, be sure to clear that property from anywhere that script is used in the Creation Kit first. Otherwise, the property will still exist on the CK object (but you won't be able to see it) and the game will still try to load the property. If you forget to do this, you can remove the zombie property in xEdit.
- Before making a new script, see if there's an existing default script you can use, e.g.
defaultSetStageOnDeath
ordefaultPlayerEnableDisableLinkedRef
. - Don’t start properties, variables, functions, or any type of Papyrus identifier with a number --- the compiler will fail (i.e.
123function()
fails). - Avoid constant polling with short intervals whenever possible. There are almost always more efficient ways of doing something, such as events that detect what you are trying to poll. If you use a while loop or
OnUpdate
, make sure there is some failsafe to exit the loop/stop the updates. - Avoid long
Wait()
functions (this can cause issues with stack dumping: https://ck.uesp.net/wiki/User:DavidJCobb/Stack_dumping). Instead, useRegisterForSingleUpdate
/OnUpdate
orRegisterForSingleUpdateGameTime
/OnUpdateGameTime
. - If you make an ObjectReference or an Actor a property, that ref will become persistent (always loaded). Avoid this when possible and use linkedrefs or referencealiases.
- If you must use an Actor or ObjectReference property, try to use a reference alias instead, as these clear automatically when the quest is done. Example:
ReferenceAlias Property Hentus_alias Auto
, then later(Hentus_alias.GetReference() as actor).AddItem()
. (See Skyrim quests for examples.) - More information on levels of persistence can be found here: https://www.afkmods.com/index.php?/topic/4250-skyrim-levels-of-persistence/ and more information on how to avoid persistence can be found here: https://ck.uesp.net/wiki/Persistence_(Papyrus)
- When testing a script, if you choose to COC into the cell, COC into a cell NEAR the one where your script will be triggered, not in it, or it might bug out – better yet, just travel to the trigger area in the same way a real player would.
- If you’re making a do-once script, use states and make sure to send the script to an empty state when it is done running.
- If you’ve got an empty state, make sure it remains empty; otherwise the event in it will still be processed, which takes away a big benefit to empty states (not having events even be processed).
- Avoid piling too much onto your script at once, especially if it has to be run often – too much activity can be hard on Papyrus. States can be useful for limiting what is running at a given point in time.
- Avoid convenience functions – use
GetReference() as Actor
instead ofGetActorRef()
,GetActorValue()
instead ofGetAV()
, etc... - Sometimes there are Condition Functions that do not have Papyrus equivalents (e.g.
IsPlayerInRegion
). If you need to access these conditions in a script, two possibilities are:- Use a spell: Make a constant effect magic effect that sets a global to 1 on effect start and 0 on effect end. Then, make an ability that has this effect, and condition the effect using the condition function you want to use in your Papyrus script. Add the ability to the player (or to a player quest alias). Then, add this global variable as a script property. You can check if it is equal to 1 or 0 to see if your condition is met.
- Use a quest: Make a quest with reusable stages, then make a stage with multiple log entries. Condition the different entries using the condition function you need to check, and use quest fragments for the different entries for how you want your script to respond to the different conditions. Then, when you need to check the condition, use the SetStage command on your new quest. The quest will evaluate the conditions, and response with the appropriate fragment.
- Condition functions generally process faster than their Papyrus equivalents. For a large script, using the steps in (20) can also be a good way to lighten the load of your script.
- More generally: Don't use Papyrus for something that can be done without papyrus.
- COMMENT 👏 YOUR 👏 CODE 👏
- Performance: Profile your scripts and functions to understand the time delay by your scripts and functions. You can use speedscope to visualize your profiles. https://www.reddit.com/r/skyrimmods/comments/i57siz/profiling_your_papyrus_scripts/