O TEMA DO FÓRUM ESTÁ EM MANUTENÇÃO. FEEDBACKS AQUI: ACESSAR

RGSS3 DIY - Menu Circular

Iniciado por Brandt, 31/07/2019 às 22:51

31/07/2019 às 22:51 Última edição: 01/08/2019 às 01:26 por Brandt
RGSS3 DIY: Menu circular







para RPG Maker VX Ace
por Masked

#invernoCRM

Sumário




1 Introdução




O RPG Maker VX Ace usa a linguagem Ruby para a programação de seus scripts. Em cima do Ruby, a engine usa o RGSS3 (Ruby Game Scripting System 3), um framework que adiciona funcionalidades gráficas, de áudio e de controle e, basicamente, permite criar jogos com a linguagem.

Além do RGSS3, o RPG Maker também vem com um conjunto bem extenso de scripts padrão, que implementam funcionalidades como Personagens, Inimigos, Habilidades, Cenas, Janelas, etc., e são essenciais para criar jogos de RPG com a engine.

Basicamente, esses scripts são o que faz tudo que o desenvolvedor cria na engine funcionar como esperado.

Porém, o grande poder de ter um sistema de scripting na engine não é simplesmente ter esse código funcionando: é a capacidade de extender suas funcionalidades através de scripts criados pelo próprio desenvolvedor. É aí que entra o conteúdo dessa aula.

A ideia aqui não é ensinar a programar em Ruby, e sim usar os recursos do RGSS3 e dos scripts padrão do RPG Maker VX Ace de forma eficiente, através de um tutorial guiado com explicações passo a passo. Com isso, espero que fique mais clara a forma como são escritos scripts para a engine, e que com um pouco de conhecimento de Ruby você já seja capaz de montar um script bonito com RGSS3.

Nessa aula, criaremos um script de Menu Circular. É um script bem conhecido, e o resultado final fica parecido com isso:





Fonte: Ring menu 3D do Gab!


Acho que é um bom script para ponto de partida. No futuro talvez eu faça mais alguns, pra incluir mais coisas, mas por enquanto acho que esse dá conta.

2 Estrutura geral do script




Pois bem, vamos à parte de botar a mão na massa.

O primeiro passo é descobrir onde devemos mexer para alterar o menu, que é nosso objetivo. Não precisa ser muito experiente nem especialmente observador para saber que a cena de menu é gerenciada na classe Scene_Menu.

2.1 Scene_Menu

A cena de menu será então nosso ambiente de trabalho. Tudo que quisermos alterar no menu será feito dentro da classe Scene_Menu.

2.1.1 Ciclo de vida

No RPG Maker VX Ace, todas as classes Scene herdam de Scene_Base. Essa classe define algumas funções que são chamadas em todas as cenas, e controlam o que acontece em diferentes momentos do ciclo de vida da cena. São elas:

  • start: Chamada assim que a cena é chamada, usada para criar os objetos da   cena
  • post_start: Chamada após o início da cena, e após criados seus objetos. Na   classe Scene_Base, é aqui que acontece a transição das cenas.
  • update: Possivelmente a função mais importante. É chamada a todo frame   enquanto a cena está ativa, e é responsável por gerenciar toda a interação   do jogador com a cena.
  • update_basic: Acontece junto da update, e seu intuito é atualizar objetos   da cena que não têm a ver com a lógica da interação com a cena (i.e.   janelas, sprites e etc.).
  • pre_terminate: Acontece logo antes de finalizar a cena, antes de os objetos   serem tirados de tela. Usada principalmente para transições entre cenas.
  • terminate: Finaliza a cena. Aqui destroem-se os objetos criados no start e   tudo que tem a ver com a cena é removido para dar lugar à próxima.
Podemos visualizar o ciclo de vida da cena através do seguinte diagrama:


De certa forma, podemos juntar as funções start e post_start em uma só, assim como a pre_terminate e terminate e a update_basic e update. A divisão dessas funções é feita apenas como forma de organização, e para a execução do script pouco importa se ela acontece de fato.

Assim, basicamente temos três momentos essenciais na nossa cena: start, update e terminate.

Vamos começar então sobreescrevendo essas três funções na classe Scene_Menu:

Código: ruby
#==============================================================================
# ** Scene_Menu
#------------------------------------------------------------------------------
#  Esta classe executa o processamento da tela de menu.
#==============================================================================
class Scene_Menu < Scene_MenuBase
  #--------------------------------------------------------------------------
  # * Inicialização do processo
  #--------------------------------------------------------------------------
  def start
    super
  end
  #--------------------------------------------------------------------------
  # * Atualização da tela
  #--------------------------------------------------------------------------
  def update
    super
  end
  #--------------------------------------------------------------------------
  # * Finalização do processo
  #--------------------------------------------------------------------------
  def terminate
    super
  end
end


Com isso, tente abrir o menu no jogo. O resultado deve ser algo assim:



Note também que não conseguimos mais sair da cena de menu. Isso acontece porque, originalmente, a ação de sair do menu é responsabilidade da janela de comandos do menu (Window_MenuCommand), que detecta o pressionamento da tecla X e volta para a cena anterior. Mas nós não criamos a janela de comandos na cena, então não temos como sair dela!

Vamos contornar isso mais pra frente, adicionando à cena um objeto análogo à janela de comandos.

2.1.2 Objetos da cena

Vamos então ao planejamento dos objetos da cena.

Primeiro, precisamos decidir como queremos que a cena se pareça. Com isso, podemos estimar quais objetos vamos precisar colocar nela e como organizar eles.

Por sorte, temos vários scripts desse tipo por aí para usar de referência!

No geral, o que queremos num script de menu circular é:

  • o jogador, no meio da tela e sem blur
  • Um anel de ícones em volta do jogador, um para cada opção do menu
  • Algum tipo de informação adicional, como o dinheiro do jogador
Bem simples, certo?

Podemos usar Sprites para o jogador e para o anel de ícones e uma Window (possivelmente com fundo transparente, para combinar com a cena) para o dinheiro.

No geral, quando usamos vários Sprites em uma cena, no RPG Maker, é comum criar um Spriteset, que é um objeto que gerencia esses Sprites no lugar da cena. Isso evita poluir a classe da cena com lógica de atualização dos gráficos, que realmente não faz parte da lógica da cena.

No nosso caso, temos alguns sprites (um para o personagem, e vários para os comandos do menu), mas o personagem aqui é estático, e os sprites do menu serão gerenciados por outro objeto (mais sobre isso mais pra frente), então não há muito ganho em criar um Spriteset para a cena, mas criaremos um mais pra frente.

Vamos ao código.

3 Implementação




3.1 Sprite do jogador

Vamos adicionar uma função create_player à cena, que cria o sprite do jogador. O código para ela é bem direto:

Código: ruby
#--------------------------------------------------------------------------
# * Cria o sprite do jogador
#--------------------------------------------------------------------------
def create_player
  @player = Sprite_Character.new(@viewport, $game_player)
end


Aqui usamos vários elementos dos scripts prontos do RMVXAce:

  • Sprite_Character: É uma classe especializada em desenhar sprites de   personagens, e já lida com posição, animação, recortar o bitmap, etc.
  • @viewport: Viewport é uma classe do RGSS que permite isolar sprites, e tem   funcionalidades como restringir a área da tela e aplicar efeitos em sprites   em conjunto. O @viewport é comum a todas as cenas e é criado na função   create_main_viewport da Scene_Base.
  • $game_player: É uma variável global com o Game_Character do jogador, que   cai como uma luva no que precisamos aqui.
Claro, apenas criar a função não vai fazer nada. Temos que chamar ela no start para criar nosso sprite:

Código: ruby
#--------------------------------------------------------------------------
# * Inicialização do processo
#--------------------------------------------------------------------------
def start
  super
  create_player
end


Também é importante atualizar o sprite quando atualizamos a cena:

Código: ruby
#--------------------------------------------------------------------------
# * Atualização da tela
#--------------------------------------------------------------------------
def update
  super
  @player.update
end


Da mesma forma, é importante dispor o sprite quando terminamos a cena:

Código: ruby
#--------------------------------------------------------------------------
# * Finalização do processo
#--------------------------------------------------------------------------
def terminate
  super
  @player.dispose
end


Veja que, com isso, quando abrimos o menu, o jogador aparece por cima do fundo escurecido da cena:



Isso acontece porque, agora, o sprite do jogador é um objeto da cena de menu.

Também é importante notar que não necessariamente o sprite do jogador estará no centro da tela. Por exemplo:




3.2 Janela de dinheiro

Nesse passo, nós adicionaríamos uma função create_gold_window à cena, que criaria a janela de dinheiro. Por sorte, essa função já existe na Scene_Menu padrão!

Podemos só adicionar ela ao start:

Código: ruby
#--------------------------------------------------------------------------
# * Inicialização do processo
#--------------------------------------------------------------------------
def start
  super
  create_player
  create_gold_window
end


O resultado:



Essa janela fica meio estranha na cena, no entanto. Não vamos usar janelas com fundo em nenhum outro lugar, então fica estranho usar só ali.

Para resolver isso, vamos modificar mais uma classe: a Window_Gold.

Código: ruby
#==============================================================================
# ** Window_Gold
#------------------------------------------------------------------------------
#  Esta janela exibe a quantia de dinheiro.
#==============================================================================
class Window_Gold < Window_Base
end


Queremos modificar a opacidade do fundo da janela. Para isso, precisamos alterar a propriedade opacity dela assim que for criada.

Faremos isso modificando o método initialize da Window_Gold. Mas não podemos simplesmente apagar o método já existente, porque ele faz coisas para inicializar a janela, e que queremos que aconteçam. Também não é interessante duplicar o código, porque a chance de introduzir incompatibilidades com outros scripts com isso é alta.

Então criamos um alias para o método existente (uma cópia dele com outro nome) e chamamos ele no nosso novo initialize:

Código: ruby
#==============================================================================
# ** Window_Gold
#------------------------------------------------------------------------------
#  Esta janela exibe a quantia de dinheiro.
#==============================================================================
class Window_Gold < Window_Base
  #--------------------------------------------------------------------------
  # * Aliases
  #--------------------------------------------------------------------------
  alias ring_menu_initialize initialize
  #--------------------------------------------------------------------------
  # * Inicialização do objeto
  #--------------------------------------------------------------------------
  def initialize
    ring_menu_initialize
    self.opacity = 0
  end
end


Veja que, logo após chamarmos o método copiado, definimos a opacidade da janela para o valor que queríamos.

O resultado é o seguinte:



Podemos ainda dar uma incrementada nessa janela adicionando um ícone de moeda nela, que tal?

Para isso, vamos primeiro criar um módulo de configuração logo no início do script. É importante que esse módulo tenha um nome único para evitar problemas de compatibilidade com configurações de outros scripts, então Config, por exemplo é um nome bem ruim.

No geral, o módulo de configuração tem o nome do script, pois é bem improvável que existam dois scripts com o mesmo nome no projeto. No nosso caso, vamos chamar ele então de RingMenu.

Dentro do módulo, criamos uma constante GOLD_ICON que define o índice do ícone que usaremos na janela de dinheiro:

Código: ruby
#==============================================================================
# ** RingMenu
#------------------------------------------------------------------------------
#  Este modulo define as configurações do menu circular.
#==============================================================================
module RingMenu
  # Ícones
  GOLD_ICON     = 361
end


Usar módulos com constantes para esse tipo de coisa permite maior personalização do script e evita deixar "valores mágicos" no código, que ninguém sabe para que serve.

Agora que temos nossa constante para o ícone, podemos modificar a função refresh da Window_Gold para desenhar ele na janela. O refresh é o método das classes Window onde é feita a renovação do desenho da janela, e praticamente todas as classes de janela têm ele, apesar da Window_Base não definí-lo.

Não confunda esse método com o update: assim como nas Scenes, o update das Windows é chamado a todo frame, e fazer coisas caras como desenhar texto nesse método pode causar problemas de performance. O método refresh é chamado apenas quando realmente for necessário atualizar o conteúdo da janela.

Assim como o initialize, queremos manter o código já existente para o refresh. Criamos então um alias, e adicionamos o desenho do ícone ao final do método:

Código: ruby
#--------------------------------------------------------------------------
# * Aliases
#--------------------------------------------------------------------------
alias ring_menu_refresh     refresh
#--------------------------------------------------------------------------
# * Renovação
#--------------------------------------------------------------------------
def refresh
  ring_menu_refresh
  draw_icon(RingMenu::GOLD_ICON, 0, 0)
end


A função draw_icon é definida na Window_Base, e recebe como parâmetro, respectivamente: o índice do ícone, a posição X e a posição Y onde desenhar ele.

Feito isso, nossa classe Window_Gold fica assim:

Código: ruby
#==============================================================================
# ** Window_Gold
#------------------------------------------------------------------------------
#  Esta janela exibe a quantia de dinheiro.
#==============================================================================
class Window_Gold < Window_Base
  #--------------------------------------------------------------------------
  # * Aliases
  #--------------------------------------------------------------------------
  alias ring_menu_initialize  initialize
  alias ring_menu_refresh     refresh
  #--------------------------------------------------------------------------
  # * Inicialização do objeto
  #--------------------------------------------------------------------------
  def initialize
    ring_menu_initialize
    self.opacity = 0
  end
  #--------------------------------------------------------------------------
  # * Renovação
  #--------------------------------------------------------------------------
  def refresh
    ring_menu_refresh
    draw_icon(RingMenu::GOLD_ICON, 0, 0)
  end
end


E agora nossa janela tem um ícone de moeda!



Com isso, terminamos a janela de dinheiro. Agora precisamos fazer o menu em si.

3.3 Anel de comandos

Tudo isso é muito bonito, mas até agora não fizemos o menu de verdade!

Eu deixei essa parte por último justamente porque é a que dá mais trabalho. Para esse objeto, vamos ter que criar várias classes: Uma para gerenciar os comandos, uma para gerenciar o anel, e uma para os sprites dos ícones e nomes de cada comando.

Além disso, com alguns comandos do menu precisam selecionar um personagem do grupo, precisamos que o anel sirva também para esse fim.

Mãos à obra então.

3.3.1 CommandRing

Criaremos uma classe CommandRing, que representa o anel de comandos:

Código: ruby
#==============================================================================
# ** CommandRing
#------------------------------------------------------------------------------
#  Esta classe representa um anel de comandos do menu circular.
#==============================================================================
class CommandRing
  #--------------------------------------------------------------------------
  # * Inicialização do objeto
  #--------------------------------------------------------------------------
  def initialize
  end
  #--------------------------------------------------------------------------
  # * Atualização do objeto
  #--------------------------------------------------------------------------
  def update
  end
  #--------------------------------------------------------------------------
  # * Disposição do objeto
  #--------------------------------------------------------------------------
  def dispose
  end
end


Essa classe é responsável por agrupar toda a funcionalidade do anel de comandos em um único objeto.

É dentro da classe CommandRing que trataremos os controles do jogador para interagir com o menu, e manteremos o outro objeto responsável por desenhar os comandos na tela.

Para isso, vamos criar uma estrutura parecida com a da Window_Command que vem por padrão no RMVXAce. Na verdade, como a Window_Command já tem muito do que precisamos pronto, vamos herdar dela na nossa classe.

Idealmente não faríamos isso, pois não podemos dizer que CommandRing é um Window_Command. Infelizmente, o RPG Maker junta funcionalidades de janela e de tratador de comandos na mesma classe, então estamos de mãos atadas. Ou herdamos da classe errada, ou replicamos um montão de código.

Também precisamos evitar criar uma janela, que acontece automaticamente quando criamos uma Window_Command. Por isso, também temos que sobrescrever o initialize e mais algumas funções da classe que usam a classe Window do RGSS.

Código: ruby
#==============================================================================
# ** CommandRing
#------------------------------------------------------------------------------
#  Esta classe representa um anel de comandos do menu circular.
#==============================================================================
class CommandRing < Window_MenuCommand
  #--------------------------------------------------------------------------
  # * Atributos
  #--------------------------------------------------------------------------
  attr_accessor   :active
  #--------------------------------------------------------------------------
  # * Inicialização do objeto
  #--------------------------------------------------------------------------
  def initialize
    clear_command_list
    make_command_list
    @active = true
    @index = 0
    @handler = {}
  end
  #--------------------------------------------------------------------------
  # * Verifica se o menu está aberto
  #--------------------------------------------------------------------------
  def open?
    true
  end
  #--------------------------------------------------------------------------
  # * Atualização do objeto
  #--------------------------------------------------------------------------
  def update
    process_cursor_move
    process_handling
  end
  #--------------------------------------------------------------------------
  # * Disposição do objeto
  #--------------------------------------------------------------------------
  def dispose
  end
end


Explicando: no initialize, fazemos algumas coisas:

  • Chamamos clear_command_list e make_command_list, para inicializar os comandos. A Window_Command precisa desse passo
  • Criamos uma variável @active que tem um attr_accessor. Essa variável substitui o active da classe Window do RGSS que a Window_Command usa e gera problemas, pois nosso objeto não é uma Window
  • Criamos um método open? que sobrescreve o da classe Window e sempre retorna true.
  • Criamos uma variável @index que começa em 0 e @handler que é um Hash. Essas variáveis são usadas pela Window_Command através da Window_Selectable para gerenciar a seleção dos comandos e a execução deles.
No update, chamamos process_cursor_move e process_handling, que são duas funções da Window_Selectable que a Window_Command herda e que precisam ser executadas para atualizar o item selecionado e a execução dos comandos quando clicados.

Veja também que herdamos não só de Window_Command, mas sim de Window_MenuCommand. Isso porque essa classe já tem a construção da lista de comandos que queremos no menu.

Precisamos também, claramente, criar a janela na nossa cena. Para isso, definimos uma função create_command_ring e chamamos ela no método start da Scene_Menu:

Código: ruby
#--------------------------------------------------------------------------
# * Inicialização do processo
#--------------------------------------------------------------------------
def start
  super
  create_player
  create_gold_window
  create_command_ring
end
#--------------------------------------------------------------------------
# * Cria o anel de comandos
#--------------------------------------------------------------------------
def create_command_ring
  @command_window = CommandRing.new
end


Veja que não precisamos nos preocupar (nem devemos) em atualizar o nosso objeto, pois ele é uma janela do ponto de vista da cena, e toda cena que herda de Scene_Base atualiza automaticamente todas as variáveis de instância que herdam do tipo Window. A mesma coisa vale para a disposição do objeto.

Do jeito que está, nosso CommandRing é basicamente uma Window_Command invisível. Precisamos modificar algumas funções para fazer com que essa classe se comporte da forma que queremos: um anel de opções.

Fazer isso é bem simples: precisamos apenas tornar nosso índice um índice circular, que é um nome inventado para um índice que volta para o começo uma vez que passa do final, e vai pro final quando volta para antes do começo.

Podemos implementar isso facilmente usando o operador de resto da divisão %:

Código: ruby
#--------------------------------------------------------------------------
# * Avança um item
#--------------------------------------------------------------------------
def next_item
  @index += 1
  @index %= @list.size
end
#--------------------------------------------------------------------------
# * Volta um item
#--------------------------------------------------------------------------
def prev_item
  @index -= 1
  @index %= @list.size
end


E aí modificamos a função process_cursor_move para usar essas funções:

Código: ruby
#--------------------------------------------------------------------------
# * Execução da rotação do anel
#--------------------------------------------------------------------------
def process_cursor_move
  return unless cursor_movable?
  dir = Input.dir4
  return if dir.zero?
  Sound.play_cursor
  return prev_item if dir < 5
  return next_item
end


Nessa função usamos mais dois elementos do RGSS e dos scripts padrão: os módulos Input e Sound.

O módulo Input é o responsável pela comunicação do jogo com os controles. É através dele que detectamos teclas pressionadas.

Nesse caso, usamos a função dir4, que retorna um código direcional: 2 = cima, 4 = esquerda, 6 = direita, 8 = baixo, ou 0 caso nenhuma tecla direcional esteja pressionada. A condição dir < 5 verifica se a direção é "cima" ou "esquerda", e nessa caso giramos a roda no sentido anti-horário (voltamos um item). Se não, giramos a roda no sentido horário (avançamos um item).

O módulo Sound é um dos módulos padrão do RMVXAce, e tem funções para tocar efeitos sonoros comuns, como movimento do cursor, confirmação, cancelamento, etc.

Outra coisa que queremos é adicionar um tempo de rotação para o anel. Afinal, gostaríamos que a rotação tivesse uma animação e não fosse imediata (até porque se for imediata, fica difícil acertar as opções!)

Vamos criar uma constante SPIN_DURATION para configurar o tempo de rotação:

Código: ruby
#==============================================================================
# ** RingMenu
#------------------------------------------------------------------------------
#  Este modulo define as configurações do menu circular.
#==============================================================================
module RingMenu
  # Ícones
  GOLD_ICON     = 361

  # Tamanho da fonte
  FONT_SIZE     = 24

  # Duração da rotação
  SPIN_DURATION = 16
end


Agora adicionamos à CommandRing uma variável @spin que servirá de temporizador para a rotação:

Código: ruby
#--------------------------------------------------------------------------
# * Atributos
#--------------------------------------------------------------------------
attr_accessor   :active
attr_reader     :spin
#--------------------------------------------------------------------------
# * Inicialização do objeto
#--------------------------------------------------------------------------
def initialize
  clear_command_list
  make_command_list
  @active = true
  @index = 0
  @handler = {}
  @spin = 0
end


Precisamos também começar a contagem da rotação quando avançarmos/voltarmos um item:

Código: ruby
#--------------------------------------------------------------------------
# * Avança um item
#--------------------------------------------------------------------------
def next_item
  @index += 1
  @index %= @list.size
  @spin = -RingMenu::SPIN_DURATION
end
#--------------------------------------------------------------------------
# * Volta um item
#--------------------------------------------------------------------------
def prev_item
  @index -= 1
  @index %= @list.size
  @spin = RingMenu::SPIN_DURATION
end


Observe que na função next_item definimos um valor negativo para @spin. Usamos o sinal para indicar que a rotação é no sentido anti-horário. Você verá como isso facilita nossa vida quando implementarmos a animação mais pra frente.

Claro, para que o temporizador seja de fato um temporizador também temos que diminuir o valor dele a cada frame. Adicionamos à update uma chamada para a função update_spin:

Código: ruby
#--------------------------------------------------------------------------
# * Atualização do objeto
#--------------------------------------------------------------------------
def update
  process_cursor_move
  process_handling
  update_spin
end
#--------------------------------------------------------------------------
# * Atualização da rotação
#--------------------------------------------------------------------------
def update_spin
  @spin -= @spin <=> 0
end


No update_spin, usamos a expressão @spin <=> 0 com o spaceship operator para diminuir o módulo do contador (lembre que ele pode ser negativo, então não podemos simplesmente subtrair 1 dele).

A função cursor_movable? é da classe Window_Selectable, e retorna verdadeiro se podemos mover o cursor. No nosso caso, podemos movê-lo sempre que a janela está ativa e o menu não está girando:

Código: ruby
#--------------------------------------------------------------------------
# * Verifica se pode mover o cursor
#--------------------------------------------------------------------------
def cursor_movable?
  active and @spin == 0
end


Agora, precisamos mostrar o anel na tela. Primeiro, vamos criar uma classe de Sprite especial para os itens do menu. Depois disso, usaremos um Spriteset para gerenciar os sprites em conjunto.

3.3.2 Sprite_CommandRingItem

É bem simples criar uma classe de Sprite:

Código: ruby
#==============================================================================
# ** Sprite_CommandRingItem
#------------------------------------------------------------------------------
#  Este sprite é usado para exibir itens no anel do menu em círculo.
#==============================================================================
class Sprite_CommandRingItem < Sprite
  #--------------------------------------------------------------------------
  # * Inicialização do objeto
  #     viewport  : camada
  #--------------------------------------------------------------------------
  def initialize(viewport)
    super(viewport)
  end
end


Agora adicionamos alguns atributos que vamos usar dentro do sprite, e chamamos uma função para criar o bitmap dele:

Código: ruby
#--------------------------------------------------------------------------
# * Inicialização do objeto
#     viewport  : camada
#     label     : rótulo
#     symbol    : comando
#--------------------------------------------------------------------------
def initialize(viewport, label, symbol)
  super(viewport)
  @label = label
  @symbol = symbol
  create_bitmap
end


Falta só a função create_bitmap, que é onde vamos construir o bitmap do sprite da forma que queremos mostrar na tela. Na verdade, vamos precisar de várias funções auxiliares, mas vamos implementar elas depois.

Em linhas gerais, a função é a seguinte:

Código: ruby
#--------------------------------------------------------------------------
# * Criação do bitmap
#--------------------------------------------------------------------------
def create_bitmap
  width = [icon_width, label_width].max
  height = icon_height + label_height
  self.bitmap = Bitmap.new(width, height)
  draw_icon
  draw_label
  self.ox = self.bitmap.width / 2
  self.oy = self.bitmap.height / 2
end


Temos três passos principais:

  • Calcular as dimensões do bitmap. No caso, a largura é a maior largura entre o ícone e o rótulo do item
  • Criar o bitmap e desenhar o ícone e o rótulo nele
  • Ajustar a posição da origem do sprite para o centro do bitmap
Precisamos ainda implementar as funções icon_width, icon_height, label_width, label_height, draw_icon e draw_label, mas veja que essa função já faz sentido. Isso é bastante comum quando programamos, onde definimos um problema grande em termos de outros problemas menores e vamos resolvendo um por vez.

Com isso, temos funções enxutas e que fazem somente o que o nome delas diz, deixando o resto para outras funções auxiliares.

As funções de largura e altura do ícone são bem bobinhas, e só retornam a largura e altura dos ícones do RPG Maker:

Código: ruby
#--------------------------------------------------------------------------
# * Largura do ícone
#--------------------------------------------------------------------------
def icon_width
  24
end
#--------------------------------------------------------------------------
# * Altura do ícone
#--------------------------------------------------------------------------
def icon_height
  24
end


Aqui pode parecer vantajoso usar constantes no lugar de funções, mas não é o caso: com funções, possibilitamos que esses valores sejam sobrescritos por uma outra classe no futuro (o que queremos, pois vamos fazer um anel para personagens também).

Para a largura e altura do rótulo, vamos usar uma constante de configuração para o tamanho da fonte no nosso módulo RingMenu:

Código: ruby
#==============================================================================
# ** RingMenu
#------------------------------------------------------------------------------
#  Este modulo define as configurações do menu circular.
#==============================================================================
module RingMenu
  # Ícones
  GOLD_ICON     = 361

  # Tamanho da fonte
  FONT_SIZE     = 24
end


Podemos então calcular a largura do texto com o método text_size da classe Bitmap do RGSS, e usar o tamanho da fonte para a altura do texto (é uma boa aproximação!)

Código: ruby
#--------------------------------------------------------------------------
# * Largura do rótulo
#--------------------------------------------------------------------------
def label_width
  bitmap = Cache.empty_bitmap
  bitmap.font.size = RingMenu::FONT_SIZE
  bitmap.text_size(@label).width
end
#--------------------------------------------------------------------------
# * Altura do rótulo
#--------------------------------------------------------------------------
def label_height
  RingMenu::FONT_SIZE
end


Veja que no label_width, para usarmos o text_size, precisamos de uma instância de Bitmap. Por isso, pegamos um bitmap vazio (o módulo padrão do VX Ace Cache tem uma função empty_bitmap pra isso), definimos o tamanho da fonte nele, e então calculamos a largura do texto.

Agora temos que definir a função draw_icon. Para isso, precisamos primeiro saber qual ícone desenhar para cada item do menu.

Faremos isso com uma constante ICONS no módulo RingMenu:

Código: ruby
#==============================================================================
# ** RingMenu
#------------------------------------------------------------------------------
#  Este modulo define as configurações do menu circular.
#==============================================================================
module RingMenu
  # Ícones
  GOLD_ICON     = 361
  ICONS         = {
    item:       192,
    skill:      8,
    equip:      164,
    status:     4,
    formation:  121,
    save:       117,
    game_end:   0
  }

  # Tamanho da fonte
  FONT_SIZE     = 24

  # Duração da rotação
  SPIN_DURATION = 16
end


A constante é um Hash onde as chaves são os símbolos dos itens do menu (vide  make_command_list da Window_MenuCommand padrão do VX Ace) e os valores são os respectivos índices dos ícones no iconset, que podem ser pegos no database em qualquer aba com um ícone:



Feito isso, temos um índice de ícone para cada item do menu e podemos desenhá-lo usando o Iconset:

Código: ruby
#--------------------------------------------------------------------------
# * Desenha o ícone
#--------------------------------------------------------------------------
def draw_icon
  iconset = Cache.system("Iconset")
  icon_index = RingMenu::ICONS[@symbol]
  rect = Rect.new(0, 0, 24, 24)
  rect.x = icon_index % 16 * 24
  rect.y = icon_index / 16 * 24
  self.bitmap.blt((width - rect.width) / 2, 0, iconset, rect)
end


Veja que usamos o módulo Cache para carregar o Iconset. É recomendado usar esse módulo principalmente para carregar bitmaps que são usados frequentemente, como o Iconset ou um Charset por exemplo. Isso porque, além de carregar a imagem, ele salva ela internamente. Dessa forma, não temos que carregar imagens mais que uma vez e economizamos memória.

O cálculo do retângulo do ícone no iconset é feito baseado no draw_icon da classe Window_Base, e o que ele faz é basicamente calcular em qual linha e coluna o ícone está e multiplicar pela altura e largura dos ícones respectivamente.

A linha do ícone é dada por icon_index / 16 (truncado), e a coluna em que o ícone está é dada por icon_index % 16, ou seja, o resto da divisão anterior. Pense nesse calculo como ir contando ícones da esquerda para a direita e de cima para baixo no Iconset.

Em seguida, usamos a função blt da classe Bitmap para recortar o retângulo do ícone que queremos do Iconset e colar sobre o nosso bitmap na posição (width - rect.width) / 2, 0. Essa posição X é um cálculo para achar a posição onde o ícone fica centralizado no bitmap do Sprite.

Por fim, definimos a função draw_label:

Código: ruby
#--------------------------------------------------------------------------
# * Desenha o rótulo
#--------------------------------------------------------------------------
def draw_label
  rect = Rect.new(0, icon_height, self.width, label_height)
  self.bitmap.draw_text(rect, @label, 1)
end


Essa função é bem direta: definimos o retângulo de desenho do rótulo, que ocupa o espaço logo abaixo do ícone, e desenhamos o texto centralizado usando o método draw_text da classe Bitmap passando 1 no último parâmetro.

3.3.3 Spriteset

Para o anel de comandos, é interessante usar um Spriteset, porque temos vários sprites (um para cada comando) e eles são coordenados todos mais ou menos da mesma forma.

Código: ruby
#==============================================================================
# ** Spriteset_CommandRing
#------------------------------------------------------------------------------
#  Classe dos comandos em anel do menu.
#==============================================================================
class Spriteset_CommandRing
  #--------------------------------------------------------------------------
  # * Inicialização do objeto
  #     viewport : camada
  #--------------------------------------------------------------------------
  def initialize(viewport = nil)
    @viewport = viewport
  end
  #--------------------------------------------------------------------------
  # * Atualização do objeto
  #--------------------------------------------------------------------------
  def update
  end
  #--------------------------------------------------------------------------
  # * Disposição do objeto
  #--------------------------------------------------------------------------
  def dispose
  end
end


Para esse spriteset, vamos querer usar informações do CommandRing que criou ele. Modificamos então o initialize para passar ele:

Código: ruby
#--------------------------------------------------------------------------
# * Inicialização do objeto
#     parent    : CommandRing
#     viewport  : camada
#--------------------------------------------------------------------------
def initialize(parent, viewport = nil)
  @parent = parent
  @viewport = viewport
end


Claro, precisamos também criar os sprites do anel. Vamos criar duas funções create_items e add_item que pegam os itens do menu e adicionam um sprite deles ao spriteset:

Código: ruby
#--------------------------------------------------------------------------
# * Limpeza dos itens
#--------------------------------------------------------------------------
def clear_items
  @items = []
end
#--------------------------------------------------------------------------
# * Criação dos itens
#--------------------------------------------------------------------------
def create_items
  for item in @parent.list
    add_item(item[:name], item[:symbol])
  end
end
#--------------------------------------------------------------------------
# * Adiciona um item ao spriteset
#     label   : rótulo do comando
#     symbol  : comando
#--------------------------------------------------------------------------
def add_item(label, symbol)
  @items.push(Sprite_RingItem.new(@viewport, label, symbol))
end


A função add_item é bem simples, e só coloca um novo Sprite_RingItem criado com os parâmetros adequads no array de itens do menu.

A função create_items já é mais elaborada: nela, usamos o objeto CommandRing que criou o Spriteset para adicionar os itens que ele tem na sua lista.

O objeto list é definido na Window_Command, e originalmente não é acessível de fora da classe. Por isso, temos que adicionar um attr_reader na classe  CommandRing:

Código: ruby
#==============================================================================
# ** CommandRing
#------------------------------------------------------------------------------
#  Esta classe representa um anel de comandos do menu circular.
#==============================================================================
class CommandRing < Window_MenuCommand
  #--------------------------------------------------------------------------
  # * Atributos
  #--------------------------------------------------------------------------
  attr_reader     :list

  # (Resto da classe...)
end


Cada item desse array é um hash no formato { :name, :symbol, :enabled, :ext }, com as informações dos comandos da janela. No nosso caso, usamos apenas o :name (rótulo do comando) e o :symbol (símbolo do comando, que usamos para pegar o ícone dele).

Tendo as funções para criar os sprites, podems chamar elas e lidar com a atualização e disposição deles conforme necessário, nas funções initialize, update e dispose do nosso Spriteset:

Código: ruby
#--------------------------------------------------------------------------
# * Inicialização do objeto
#     parent    : CommandRing
#     viewport  : camada
#--------------------------------------------------------------------------
def initialize(parent, viewport = nil)
  @parent = parent
  @viewport = viewport
  redraw
end
#--------------------------------------------------------------------------
# * Atualização do objeto
#--------------------------------------------------------------------------
def update
  @items.each(&:update)
end
#--------------------------------------------------------------------------
# * Disposição do objeto
#--------------------------------------------------------------------------
def dispose
  @items.each(&:dispose)
end


Obs.: O @items.each(&:<method>) usa um recurso da sintaxe do ruby que permite chamar a função <method> de cada item do array. É um atalho muito bem vindo pra escrever @items.each { |obj| obj.<method1}.

No initialize, chamamos um método redraw. Esse método vai ser o responsável por criar todos os itens do spriteset do zero:

Código: ruby
#--------------------------------------------------------------------------
# * Redesenho do spriteset
#--------------------------------------------------------------------------
def redraw
  dispose if @items
  clear_items
  create_items
end


Dessa forma, podemos também resetar o spriteset de fora dele, chamando essa função. Isso vai ser útil mais tarde.

Adicionamos o Spriteset_CommandRing no CommandRing:

Código: ruby
#--------------------------------------------------------------------------
# * Inicialização do objeto
#--------------------------------------------------------------------------
def initialize
  clear_command_list
  make_command_list
  @active = true
  @handler = {}
  @index = 0
  @spin = 0
  create_spriteset
end
#--------------------------------------------------------------------------
# * Criação do spriteset
#--------------------------------------------------------------------------
def create_spriteset
  @spriteset = Spriteset_CommandRing.new(self)
end


E claro, não podemos esquecer de atualizar o spriteset:

Código: ruby
#--------------------------------------------------------------------------
# * Atualização do objeto
#--------------------------------------------------------------------------
def update
  process_cursor_move
  process_handling
  update_spin
  update_spriteset
end
#--------------------------------------------------------------------------
# * Atualização do spriteset
#--------------------------------------------------------------------------
def update_spriteset
  @spriteset.update
end


E dispor ele quando não precisarmos mais:

Código: ruby
#--------------------------------------------------------------------------
# * Disposição do objeto
#--------------------------------------------------------------------------
def dispose
  @spriteset.dispose
end


Abrindo o jogo agora, temos o lindíssimo resultado que segue:



Como não definimos a posição dos nosso sprites, eles simplesmente aparecem na posição (0, 0) (canto superior esquerdo da tela). Não é bem o que queremos, então vamos melhorar isso: na função update, adicionamos uma chamada a uma função update_items que vai cuidar de posicionar os itens na tela da forma que queremos.

Código: ruby
#--------------------------------------------------------------------------
# * Atualização do objeto
#--------------------------------------------------------------------------
def update
  @items.each(&:update)
  update_items
end
#--------------------------------------------------------------------------
# * Atualização dos itens do anel
#--------------------------------------------------------------------------
def update_items
  for item, index in @items.each_with_index
    item.x = adjust_x(index)
    item.y = adjust_y(index)
    item.opacity = selected?(index) ? 255 : 125
    item.visible = @parent.active
  end
end
#--------------------------------------------------------------------------
# * Verifica se um índice está selecionado
#--------------------------------------------------------------------------
def selected?(index)
  index == @parent.index
end


Obs.: A função each_with_index funciona de forma similar ao each, mas além de dar o objeto no for, dá a posição dele na lista também.

A ideia da função update_items é bem clara: ajustar a posição dos itens, mudar a opacidade se eles estiverem selecionados (os itens não selecionados ficam semi-transparentes) e esconder eles totalmente se a janela-pai não estiver ativa (para não mostrar o anel com a janela desativada).

Assim como na função create_bitmap do nosso Sprite, a função update_items aqui está incompleta. Precisamos implementar as funções de ajuste de posição (que calculam de fato onde colocar o sprite na tela).

Pois bem, queremos desenhar os ícones em círculo em volta do jogador, certo?

Vamos então usar um pouco de trigonometria. Se você já passou pelo ensino médio, deve estar familiarizado com o círculo trigonométrico:
A ideia é a seguinte: podemos calcular qualquer ponto no círculo usando apenas três informações, a origem do círculo (ou centro), o ângulo do ponto em relação à origem, e o raio do círculo.

A fórmula é a seguinte:



Onde: O é o ponto de origem, R é o raio da circunferência e θ é o ângulo do ponto em relação à origem.

Traduzindo para Ruby:

Código: ruby
#--------------------------------------------------------------------------
# * Calcula a posição X de um item
#     index : Índice do item
#--------------------------------------------------------------------------
def adjust_x(index)
  center_x + Math.cos(adjust_angle(index)) * radius
end
#--------------------------------------------------------------------------
# * Calcula a posição Y de um item
#     index : Índice do item
#--------------------------------------------------------------------------
def adjust_y(index)
  center_y + Math.sin(adjust_angle(index)) * radius
end


Claro, novamente, temos que definir center_x, center_y, adjust_angle e radius.

Para o raio, vamos criar uma constante RADIUS no módulo RingMenu:

Código: ruby
#==============================================================================
# ** RingMenu
#------------------------------------------------------------------------------
#  Este modulo define as configurações do menu circular.
#==============================================================================
module RingMenu
  # Ícones
  GOLD_ICON     = 361
  ICONS         = {
    item:       192,
    skill:      8,
    equip:      164,
    status:     4,
    formation:  121,
    save:       117,
    game_end:   0
  }

  # Tamanho da fonte
  FONT_SIZE     = 24

  # Duração da rotação
  SPIN_DURATION = 16

  # Raio do anel
  RADIUS        = 128
end


E no spriteset:

Código: ruby
#--------------------------------------------------------------------------
# * Raio do anel
#--------------------------------------------------------------------------
def radius
  RingMenu::RADIUS
end


(Poderíamos usar a constante direto, mas nunca se sabe...)

Para o centro do spriteset, usamos as coordenadas do jogador:

Código: ruby
#--------------------------------------------------------------------------
# * Posição X do centro do anel
#--------------------------------------------------------------------------
def center_x
  $game_player.screen_x
end
#--------------------------------------------------------------------------
# * Posição Y do centro do anel
#--------------------------------------------------------------------------
def center_y
  $game_player.screen_y
end


Aqui, $game_player é uma variável global dos scripts padrão do VX Ace que se refere ao Game_Character do jogador. screen_x e screen_y são as posições X e Y do jogador na tela.
~ Masked

Por final, a função adjust_angle vai calcular o ângulo do item em relação ao centro:

Código: ruby
#--------------------------------------------------------------------------
# * Calcula o ângulo de um item no anel
#     index : Índice do item
#--------------------------------------------------------------------------
def adjust_angle(index)
  spin = @parent.spin.to_f / RingMenu::SPIN_DURATION
  relative_index = index - @parent.index
  (relative_index - spin) * Math::PI * 2 / @items.size + angle_offset
end


Aqui, fazemos alguns cálculos:

  • Calculamos a fração da animação de rotação com base no contador spin do CommandRing.
  • O índice do item relativo ao item selecionado no menu, indica de fato em qual posição do círculo fica o item. Essa é a parte que dá o efeito de rotacionar o círculo.
  • O ângulo em radianos para o índice relativo em relação à origem. Subtraímos também a fração da rotação que ainda falta acontecer desse índice, para dar o efeito de rotação suavizada. Como queremos mostrar os ícones de forma igualmente espaçada no círculo, pegamos o total de radianos na circunferência (2π) e dividimos em @items.size partes iguais, uma para cada item.
Ao final, adicionamos um deslocamento com a função angle_offset, para podermos definir onde os ícones começam a ser posicionados.

Eu achei interessante posicionar o item selecionado no ponto mais baixo do círculo, então defini o deslocamento para π / 2 (90°):

Código: ruby
#--------------------------------------------------------------------------
# * Deslocamento angular
#--------------------------------------------------------------------------
def angle_offset
  Math::PI / 2
end


Com tudo isso pronto, precisamos fazer os sprites já começarem posicionados corretamente. Para isso basta chamar o update no  redraw do Spriteset:

Código: ruby
#--------------------------------------------------------------------------
# * Redesenho do spriteset
#--------------------------------------------------------------------------
def redraw
  dispose if @items
  clear_items
  create_items
  update
end


Abrindo o jogo, agora sim temos um anel de comandos!



Mas, se você tentou usar o menu, deve ter percebido que ele não funciona. Vamos botar ele pra funcionar então.

3.4 Controladores

Na classe Scene_Menu padrão, temos essa função:

Código: ruby
#--------------------------------------------------------------------------
# * Criação da janela de comando
#--------------------------------------------------------------------------
def create_command_window
  @command_window = Window_MenuCommand.new
  @command_window.set_handler(:item,      method(:command_item))
  @command_window.set_handler(:skill,     method(:command_personal))
  @command_window.set_handler(:equip,     method(:command_personal))
  @command_window.set_handler(:status,    method(:command_personal))
  @command_window.set_handler(:formation, method(:command_formation))
  @command_window.set_handler(:save,      method(:command_save))
  @command_window.set_handler(:game_end,  method(:command_game_end))
  @command_window.set_handler(:cancel,    method(:return_scene))
end


Pelo nome, é fácil ver que ela cria a janela de comandos da cena. Mas o interessante são as várias chamadas à função set_handler da @command_window.

Cada chamada dessa define um método a ser executado quando um determinado comando for ativado. O último (:cancel) é um handler especial, e é ativado quando o botão de cancelamento (X) é pressionado.

Como nosso CommandRing tem as funções da Window_MenuCommand, podemos simplesmente copiar essas chamadas na função create_command_ring que criamos lá no começo:

Código: ruby
#--------------------------------------------------------------------------
# * Cria o anel de comandos
#--------------------------------------------------------------------------
def create_command_ring
  @command_window = CommandRing.new
  @command_window.set_handler(:item,      method(:command_item))
  @command_window.set_handler(:skill,     method(:command_personal))
  @command_window.set_handler(:equip,     method(:command_personal))
  @command_window.set_handler(:status,    method(:command_personal))
  @command_window.set_handler(:formation, method(:command_formation))
  @command_window.set_handler(:save,      method(:command_save))
  @command_window.set_handler(:game_end,  method(:command_game_end))
  @command_window.set_handler(:cancel,    method(:return_scene))
end


Pode testar: agora conseguimos sair do menu, e até abrir a cena de salvar e os itens!

Mas quando tentamos abrir o menu de Equipamentos, ou Habilidades, por exemplo, nos deparamos com o seguinte erro:



Isso acontece porque esses comandos precisam selecionar um personagem do grupo para seguir para suas respectivas cenas (afinal, precisamos ver as habilidades ou equipamentos de algum personagem, certo?)

Para isso, vamos adaptar nosso anel para selecionar membros do grupo.

3.5 Anel de personagens

Para o anel de personagens, temos boa parte do trabalho já feito. Isso porque a classe CommandRing é bem genérica, e precisa de poucas alterações para fazer o que precisamos, que é basicamente a mesma coisa, mas mostrando personagens no lugar de ícones.

Na verdade, poderíamos (deveríamos, se quisessemos nos preocupar com a orientação a objetos à risca) criar uma classe CommandRing_Base, e duas outras CommandRing_Icons e CommandRing_Characters, uma para cada propósito. Porém, isso adicionaria uma classe à equação e para pouco ganho no final, já que podemos simplesmente herdar de CommandRing e sobrescrever uma função.

3.5.1 CharacterRing

Nossa classe chamará CharacterRing:

Código: ruby
#==============================================================================
# ** CharacterRing
#------------------------------------------------------------------------------
#  Esta classe representa um anel de personagens do menu circular.
#==============================================================================
class CharacterRing < CommandRing 
  #--------------------------------------------------------------------------
  # * Variáveis públicas
  #--------------------------------------------------------------------------
  attr_reader   :pending_index            # Manter a posição (para organizar)
  #--------------------------------------------------------------------------
  # * Inicialização do objeto
  #--------------------------------------------------------------------------
  def initialize
    super
    @pending_index = -1
  end
  #--------------------------------------------------------------------------
  # * Criação do spriteset
  #--------------------------------------------------------------------------
  def create_spriteset
    @spriteset = Spriteset_CharacterRing.new(self)
  end
  #--------------------------------------------------------------------------
  # * Criação da lista de comandos
  #--------------------------------------------------------------------------
  def make_command_list
    for actor in $game_party.members
      add_command(actor.name, actor)
    end
  end
  #--------------------------------------------------------------------------
  # * Processamento de confirmação
  #--------------------------------------------------------------------------
  def process_ok
    super
    $game_party.menu_actor = $game_party.members[index]
  end
  #--------------------------------------------------------------------------
  # * Retorno à seleção anterior
  #--------------------------------------------------------------------------
  def select_last
    select($game_party.menu_actor.index || 0)
  end
  #--------------------------------------------------------------------------
  # * Definição de última posição (para organizar)
  #--------------------------------------------------------------------------
  def pending_index=(index)
    last_pending_index = @pending_index
    @pending_index = index
  end
end


Ao invés de um Spriteset_CommandRing, criamos um spriteset específico para essa classe. Afinal, o que muda é o que aparece na tela e os comandos. O comportamento é absolutamente idêntico ao CommandRing.

Também ajustamos a função process_ok, herdada lá da Window_Selectable, para configurar o personagem selecionado na party.

A variável $game_party representa o grupo do jogador, e o atributo menu_actor é usado pelas cenas que precisam selecionar um jogador no menu para identificar qual personagem tratar.

A função select_last e o atributo pending_index servem para simular uma Window_MenuStatus, que é a classe de janela usada no menu para gerenciar comandos que usam os personagens.

Em especial, o pending_index serve para o comando de formação, e permite selecionar um personagem e depois outro para trocar de posição.

Também por conta do comando de formação, temos que adicionar mais duas funções:

Código: ruby
#--------------------------------------------------------------------------
# * Redesenho do item
#     index : índice do item
#--------------------------------------------------------------------------
def redraw_item(index)
end
#--------------------------------------------------------------------------
# * Ativação da janela
#--------------------------------------------------------------------------
def activate
  super
  clear_command_list
  make_command_list
  @spriteset.redraw
end


A primeira, redraw_item, é para evitar que o método seja executado e acabe gerando erros (o original chama a função clear_item, que depende de propriedades da classe Window).

A função activate tem algumas adições: para recriar os comandos e redesenhar o spriteset. Fazemos isso para atualizar os characters quando a posição deles no grupo muda depois do comando de formação.

Novamente, precisamos adicionar o anel à Scene_Menu. Para isso, adicionamos uma chamada em start à função create_character_ring que criaremos:

Código: ruby
#--------------------------------------------------------------------------
# * Cria o anel de personagens
#--------------------------------------------------------------------------
def create_character_ring
  @status_window = CharacterRing.new
  @status_window.deactivate
  @status_window.update
end
#--------------------------------------------------------------------------
# * Inicialização do processo
#--------------------------------------------------------------------------
def start
  super
  create_player
  create_gold_window
  create_command_ring
  create_character_ring
end


Veja que a função create_character_ring desativa e atualiza a o CharacterRing logo após criá-lo. Fazemos isso para não mostrar o anel de personagens na cena antes de precisarmos de fato.

O nome @status_window pode parecer contraintuitivo, mas é justamente o nome da janela equivalente ao CharacterRing na Scene_Menu original. Aproveitamos essa variável para não ter que reescrever uma pancada de funções que usam ela, e de quebra ainda damos um up na compatibilidade do script.

Agora precisamos implementar o tal Spriteset_CharacterRing.

3.5.2 Spriteset

O spriteset é quase idêntico ao do anel de comandos, exceto pela forma como verifica itens selecionados (aqui precisamos olhar o pending_index também) e o tipo de Sprite que usa:

Código: ruby
#==============================================================================
# ** Spriteset_CharacterRing
#------------------------------------------------------------------------------
#  Classe do spriteset da seleção dos personagens em anel do menu.
#==============================================================================
class Spriteset_CharacterRing < Spriteset_CommandRing
  #--------------------------------------------------------------------------
  # * Adiciona um item ao spriteset
  #     actor : personagem (Game_Actor)
  #--------------------------------------------------------------------------
  def add_item(actor)
    @items.push(Sprite_CharacterRingItem.new(@viewport, actor))
  end
  #--------------------------------------------------------------------------
  # * Verifica se um índice está selecionado
  #--------------------------------------------------------------------------
  def selected?(index)
    super or index == @parent.pending_index
  end
end


Aqui usamos o objeto $game_party, que é uma global padrão do maker com um objeto do tipo Game_Party. Esse objeto tem os membros do grupo do jogador, que pegamos aqui através do atributo members.

Agora, fica faltando apenas a classe Sprite_CharacterRingItem, que é onde vamos de fato mudar as coisas.

3.5.3 Sprite_CharacterRingItem

Primeiro, vamos definir a classe com o básico do básico para o que queremos sobrescrever da Sprite_CommandRingItem:

Código: ruby
#==============================================================================
# ** Sprite_CharacterRingItem
#------------------------------------------------------------------------------
#  Este sprite é usado para exibir personagens no anel do menu em círculo.
#==============================================================================
class Sprite_CharacterRingItem < Sprite_CommandRingItem
  #--------------------------------------------------------------------------
  # * Inicialização do objeto
  #     viewport  : camada
  #     actor     : personagem (Game_Actor)
  #--------------------------------------------------------------------------
  def initialize(viewport, actor)
    @actor = actor
    super(viewport, actor.name, nil)
  end
  #--------------------------------------------------------------------------
  # * Desenha o ícone
  #--------------------------------------------------------------------------
  def draw_icon
  end
  #--------------------------------------------------------------------------
  # * Largura do ícone
  #--------------------------------------------------------------------------
  def icon_width
  end
  #--------------------------------------------------------------------------
  # * Altura do ícone
  #--------------------------------------------------------------------------
  def icon_height
  end
end


Aqui, o "ícone" é o gráfico do personagem. Como as dimensões e a forma de desenhar o ícone mudam dos ícones do RM para personagens, precisamos ajustar as funções que lidam com essas coisas: icon_width, icon_height e draw_icon.

Veja também que no initialize, ao invés de receber um name e um symbol simplesmente recebemos um Game_Actor, que é justamente o que queremos mostrar.

Bom, a função draw_icon é a mais complicadinha, então vamos deixar ela para depois. Primeiro, vamos implementar a icon_width e icon_height. Para isso, primeiro, vejamos como a classe padrão Spriteset_Character lida com o tamanho dos chars. Spriteset_Character, função set_character_bitmap:

Código: ruby
#--------------------------------------------------------------------------
# * Definição de bitmap do personagem
#--------------------------------------------------------------------------
def set_character_bitmap
  self.bitmap = Cache.character(@character_name)
  sign = @character_name[/^[\!\$]./]
  if sign && sign.include?('$')
    @cw = bitmap.width / 3
    @ch = bitmap.height / 4
  else
    @cw = bitmap.width / 12
    @ch = bitmap.height / 8
  end
  self.ox = @cw / 2
  self.oy = @ch
end


Aqui, os valores @cw e @ch são a largura e a altura do personagem, respectivamente. Podemos usar mesma lógica nas nossas funções icon_width e icon_height, basta dividir o cálculo nas funções:

Código: ruby
#--------------------------------------------------------------------------
# * Bitmap do charset
#--------------------------------------------------------------------------
def charset
  Cache.character(@actor.character_name)
end
#--------------------------------------------------------------------------
# * Largura do ícone
#--------------------------------------------------------------------------
def icon_width
  sign = @actor.character_name[/^[\!\$]./]
  if sign && sign.include?('$')
    charset.width / 3
  else
    charset.width / 12
  end
end
#--------------------------------------------------------------------------
# * Altura do ícone
#--------------------------------------------------------------------------
def icon_height
  sign = @actor.character_name[/^[\!\$]./]
  if sign && sign.include?('$')
    charset.height / 4
  else
    charset.height / 8
  end
end


Veja que criei a função charset para pegar o bitmap do personagem e fiz o icon_width retornar o valor que teria @cw e icon_height retornar o valor que seria de @ch. A lógica é idêntica, muda só onde ela é usada.

Nota: Você pode estar se perguntando porque não usamos o Sprite_Character de superclasse, visto que ele já tem funções pra desenhar chars. Essa seria uma abordagem possível, mas eu preferi seguir usando o Sprite_CommandRingItem mesmo porque a Sprite_Character tem coisas demais que não nos interessam para a Sprite_CharacterRingItem, e teríamos que sobrescrever elas e adicionar coisas por cima. Mas sinta-se à vontade para explorar essa ideia, talvez seja uma boa.

Na classe Sprite_CommandRingItem, a draw_icon pegava o bitmap de Iconset e recortava o ícone correspondente ao índice do ícone do comando, lembra?

Aqui, faremos um processo parecido, mas usando o Charset do personagem e o índice do char dele no set. Para isso vamos novamente usar a Sprite_Character de base, dessa vez a função update_src_rect:

Código: ruby
#--------------------------------------------------------------------------
# * Atualização do retângulo de origem
#--------------------------------------------------------------------------
def update_src_rect
  if @tile_id == 0
    index = @character.character_index
    pattern = @character.pattern < 3 ? @character.pattern : 1
    sx = (index % 4 * 3 + pattern) * @cw
    sy = (index / 4 * 4 + (@character.direction - 2) / 2) * @ch
    self.src_rect.set(sx, sy, @cw, @ch)
  end
end


Para simplificar nossa vida, criamos uma função character_rect que calcula então o retângulo, usando exatamente essa lógica:

Código: ruby
#--------------------------------------------------------------------------
# * Retângulo do char
#--------------------------------------------------------------------------
def character_rect
  index = @actor.character_index
  pattern = 1
  direction = 2
  cw = icon_width
  ch = icon_height
  sx = (index % 4 * 3 + pattern) * cw
  sy = (index / 4 * 4 + (direction - 2) / 2) * ch
  Rect.new(sx, sy, cw, ch)
end


Claro, aplicamos alguns ajustes:

  • Como não temos a variável @character, fixamos o pattern em 1 (o sprite do meio da animação) e direction em 2 (olhando para baixo).
  • Como não definimos @cw e @ch, usamos icon_width e icon_height, que na prática são a mesma coisa.
Agora, para desenhar o char no sprite, usamos a função blt do Bitmap, assim como havíamos feito antes:

Código: ruby
#--------------------------------------------------------------------------
# * Desenha o ícone
#--------------------------------------------------------------------------
def draw_icon
  x = (width - icon_width) / 2
  y = 0
  self.bitmap.blt(x, y, charset, character_rect)
end


E tadaaaa, temos um anel de personagens pronto:



E melhor: os comandos já funcionam! Como usamos a variável @status_window e sobrescrevemos as funções que precisávamos, tudo já é tratado para nós.

4 Fim!




É isso! Terminamos nosso script de menu circular (Script finalizado)

Tentei ser o mais didático possível nessa aula, mas posso ter deixado algo passar batido, então já peço desculpas adiantado x)

Em todo caso, fiquem à vontade para perguntar sobre qualquer dúvida que possa surgir, estarei disposto a ajudar.

o/

#invernoCRM
~ Masked

Olha isso  :'0':

Se a pessoa sair daqui falando que não conseguiu reproduzir o menu, ele vai merecer umas.

Você explicou até trigonometria na aula, a coisa tem imagens explicando até como funciona o índice dos ícones, sem palavras pra tudo isso!

01/08/2019 às 02:51 #3 Última edição: 01/08/2019 às 02:54 por Mestre R.
Nossa, que tópico o.O a cara do meu avatar é exatamente a cara que fiz ao ver tudo isso kkk

Lembrei do meu sistema por eventos

Realmente um baita tutorial! Obrigado por disponibiliza-los pra gente !!!

:hmm:

Mas, eita.  :o:
Não vou conseguir acompanhar agora, mas à noite farei questão de seguir passo por passo pra brincar. Meus parabéns pelo tópico. Às vezes, ver um exemplo sendo feito ajuda muito mais que teoria.