Some coding rules
In PSDK there's some coding rule to follow. We use rubocop
but with special settings (because some stuff are anoying or doesn't fit the PSDK use). You can find the Rubocop configuration here : .rubocop.yml
rubocop
wasn't used at all, we didn't try to make the script from that time comply on the rubocop
rules. We usually update the script coding aspect when we need to modify it.
The iterations
In PSDK we strongly discourage anyone to use for
loops. Ruby for
loops are syntaxic sugar used to write .each
call without writing it explicitely. Instead of for you should use :
number.times
if you need to iterate from 0 tonumber - 1
.number.upto(final)
if you need to iterate fromnumber
tofinal
(included, +1 increment).number.downto(final)
if you need to iterate fromnumber
tofinal
(included, -1 decrement).number.step(step_size, final)
if you need to iterate fromnumber
tofinal
with a step of step_size (final
is not always the last iteration value).collection.each
if you need to iterate through each item of the collection.array.each_with_index
if you need to iterate through array elements and have their index.array.index
if you need to find the index of an object (matching criteria) in an Array.enumerable.find
if you need to find a specific value (maching criteria) in an Array or an Enumerator.enumerable.any?
if you need to know if the array/enumerable contain something matching the criteria.
ℹNot giving a block is a way to detect if the array is not empty!enumerable.all?
if you need to know if the array/enumerable elements all match the criteria.enumerable.none?
if you need to know if none of the array/enumerable elements match the criteria.enumerable.select
if you need to select the element that match the criteria.array.select!
if you want to select the element that match the criteria and make the array contain only those elements.enumerable.reject
if you need to select the element that doesn't match the criteria.array.reject!
if you want to select the element that doesn't match the criteria and make the array contain only those elements.enumerable.collect
orenumerable.map
if you need to transform an enumerable into an array where the value are transformed by the block.
Example :ruby ten_to_nineteen = 10.times.collect { |i| i + 10 } # => [10, 11, 12, 13, 14, 15, 16, 17, 18, 19] hex_digits = 10.upto(15).collect { |i| i.to_s(16).upcase } # => ["A", "B", "C", "D", "E", "F"]
array.collect!
orarray.map!
to transform all the element of the array using the block.
Here's an example of using iterations instead of for
loops :
def find_height(img) first_y = img.height.times.find do |y| img.width.times.any? { |x| img.get_pixel_alpha(x, y) != 0 } end return img.height - first_y.to_i end
Here's the for version :
def find_height(img) first_y = nil for y in 0...img.height for x in 0...img.width break first_y = y if img.get_pixel_alpha(x, y) != 0 end break if first_y end return img.height - first_y.to_i end
The nil values
In Ruby the instance variables, global variables contain nil
if they're not assigned. This often cause issues like NoMethodError (undefined method 'meth' for nil:NilClass)
to prevent that there's various way that can be used.
Default value asignment
To assign a default value to a variable (when it's not supposed to be nil
and we're not in #initialize
) you have two legit solution :
@var = value if @var.nil?
or
@var = value unless @var
The first way is sub-optimal if the variable isn't supposed to be a boolean (ruby will have to call nil?
before setting the value). The second way is better but not the recommanded way according to rubocop
.
If you need to un-nil a variable use the ||=
operator :
@var ||= value
unless @var
, it's actually faster than ||= but you should expect rubocop
to raise an error.
Safe navigation
The safe navigation is something that was introduced in Ruby 2.3. It allows to call methods or chain call methods without adding a bunch of condition thanks to the &.
keyword.
Here's some example :
@system.update if @system
can be written like :
@system&.update
if @system && @system.things && @system.things.other
can be written like :
if @system&.things&.other
&.
keyword is special because it only check if the value on left is nil
. If it's nil, the &.
chain is broken and the result value is nil
which doesn't validate the condition.
&.
chaining is not suitable for comparison since it can return nil
unless you know what you're doing, use the safe navigation for method calling. Note that in Ruby expr.> value
is valid syntax thus `expr&.> value won't cause an error if expr is nil but it's ugly.
Array creation and operation
If you come from the Essentials you probably use the push
method from an empty array to start filling information (because of the for
thing or just because you saw that in other scripts).
In PSDK we'll rarely use the push
method. We prefer using <<
instead (it works for both array and strings) and in Ruby 2.5 it has its own opcode
(opt_ltlt).
For the array creation we prefer pre-filling the array instead of pushing each value one-by-one.
Example :
ary = [first_thing, second_thing, third_thing]
Instead of :
ary = [] ary.push first_thing ary.push second_thing ary.push third_thing
In Ruby the two methods produce the same result but aren't executed the same way. (The first one use specific opcode
that are a bit faster than calling push
).
For array that contain complex values like the list of Pokémon Info Box (in the party for example) we'll use Array.new(size) { |index| thing_to_push }
. This way of creating an array allows us to execute code before adding each value.
Example :
party_boxes = Array.new(6) { |index| create_info_box(index) }
Instead of :
party_boxes = [] for index in 0...6 party_boxes.push create_info_box(index) end
next(value)
to explicitely say what's stored.
<<
to add value to an array (especially when the conditions that gives the value existence in the array are complexe).
Explicitness
As you may have seen in Array creation and operation we'll prefer using explicit code. This is why in the newly produced code you'll see a bunch of method like create_things
instead of dosen of line of codes in the blocks.
When you can, try to factorize your code in smaller methods. This allow various thing : We know what the code is supposed to do thanks to the method name. We can customize the things using monkey patch/specialization without having to rewrite everything.
In the rubocop
rule used by PSDK we disabled the rules about implicit return
/next
. If a method or a block should return a value to the thing that called it and use more than one line, put the return
/next
keyword (according to if it's a method or a block). It helps a lot to read the code.
Factorization
In the new script, we try to factorize the code as much as possible. Factorization is the process of spliting a single method or a part of a method into various methods. The best example of factorization would be the create_graphics
method in GamePlay scenes :
def create_graphics @viewport = Viewport.create(:main, 500) @base_ui = UI::GenericBase.new(@viewport, button_texts) @mouse_button_cancel = @base_ui.ctrl.last @sub_background = Sprite.new(@viewport).set_bitmap('tcard/background', :interface) @trainer_sprite = Sprite.new(@viewport) .set_bitmap(PLAYER_SPRITE_NAME[$trainer.playing_girl], :interface) # Adjust the origin of the sprite since the TCard has a smaller surface for the sprite @trainer_sprite.set_origin((@trainer_sprite.width - PLAYER_SURFACE.first) / 2, (@trainer_sprite.height - PLAYER_SURFACE.last) / 2) @trainer_sprite.set_position(*PLAYER_COORDINATES) end
Will be factorized into :
def create_graphics create_viewport create_base_ui create_sub_background create_trainer_sprite end def create_viewport @viewport = Viewport.create(:main, 500) end def create_base_ui @base_ui = UI::GenericBase.new(@viewport, button_texts) @mouse_button_cancel = @base_ui.ctrl.last end def create_sub_background @sub_background = Sprite.new(@viewport).set_bitmap('tcard/background', :interface) end def create_trainer_sprite @trainer_sprite = Sprite.new(@viewport) .set_bitmap(PLAYER_SPRITE_NAME[$trainer.playing_girl], :interface) # Adjust the origin of the sprite since the TCard has a smaller surface for the sprite @trainer_sprite.set_origin((@trainer_sprite.width - PLAYER_SURFACE.first) / 2, (@trainer_sprite.height - PLAYER_SURFACE.last) / 2) @trainer_sprite.set_position(*PLAYER_COORDINATES) end
The factorization takes more lines (and is probably a bit slower) but it gives various advantages. It allows thiner monkey-patch and makes the specialization process a bit more easy.
Turn possible parameters into methods
Even if the factorization gives a bit help for specialization, that's not the complete solution. There's one thing we can add to this. In the previous method we factorized a block of code into the create_trainer_sprite
method, but in this method we have a really important parameter : the image used. The current version cannot show player variation (haircut for example) or other trainer because we used a constant for the trainer image name. Here's a version that is a bit more specific and allows to create trainer variations :
def create_trainer_sprite @trainer_sprite = Sprite.new(@viewport).set_bitmap(trainer_image, :interface) # [...] end def trainer_image PLAYER_SPRITE_NAME[$trainer.playing_girl] end
This version also takes more line than the previous one but you can now change trainer_image
to use the right trainer image according to your own rules (instead of just male/female).
trainer_image
method inside PFM::Trainer
to make the trainer card a bit more dynamic. You should try to answer the question Am I putting this method in the right class ?
before simply create a method in the same class for some parameters.