Quod erat demonstrandum
[anchor=osOgFAbiTNCNWYlq][/anchor]RGSS3 DIY: Menu circular
RPG Maker VX Ace
[anchor=QSEyhyFqgAdvUKoI][/anchor]Sumário
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:
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.
[anchor=YOvXbSpnPgZVtJoe][/anchor]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.
[anchor=ZxUeoXjGPMVENeiC][/anchor]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.
[anchor=fOtjTVtTEzKSFjow][/anchor]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:
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:
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.
[anchor=MKhQlWcOaWwGDAFX][/anchor]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 é:
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.
[anchor=HJNNUAqLuyBglSst][/anchor]3 Implementação
[anchor=BCaFsvQOMUXQeJBw][/anchor]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:
Aqui usamos vários elementos dos scripts prontos do RMVXAce:
Também é importante atualizar o sprite quando atualizamos a cena:
Da mesma forma, é importante dispor o sprite quando terminamos a cena:
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:
Vamos ter que tomar cuidado com isso para não desenhar as opções do menu para fora da tela.
[anchor=PKoyHeqFRWKgTdTc][/anchor]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:
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.
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:
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:
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:
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:
E agora nossa janela tem um ícone de moeda!
Com isso, terminamos a janela de dinheiro. Agora precisamos fazer o menu em si.
[anchor=oBlfPrdBUGugarLv][/anchor]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.
[anchor=qgKkDcuXLWTueyFj][/anchor]3.3.1 CommandRing
Criaremos uma classe CommandRing, que representa o anel de comandos:
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.
Explicando: no initialize, fazemos algumas coisas:
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:
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 %:
E aí modificamos a função process_cursor_move para usar essas funções:
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:
Agora adicionamos à CommandRing uma variável @spin que servirá de temporizador para a rotação:
Precisamos também começar a contagem da rotação quando avançarmos/voltarmos um item:
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:
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:
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.
[anchor=ospkVtkLXjwfKlds][/anchor]3.3.2 Sprite_CommandRingItem
É bem simples criar uma classe de Sprite:
Agora adicionamos alguns atributos que vamos usar dentro do sprite, e chamamos uma função para criar o bitmap dele:
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:
Temos três passos principais:
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:
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:
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!)
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:
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:
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:
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.
[anchor=KsltUvkiwjfEycho][/anchor]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.
Para esse spriteset, vamos querer usar informações do CommandRing que criou ele. Modificamos então o initialize para passar ele:
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:
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:
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:
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:
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:
E claro, não podemos esquecer de atualizar o spriteset:
E dispor ele quando não precisarmos mais:
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.
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:
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:
E no spriteset:
(Poderíamos usar a constante direto, mas nunca se sabe...)
Para o centro do spriteset, usamos as coordenadas do jogador:
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.
RPG Maker VX Ace
[anchor=QSEyhyFqgAdvUKoI][/anchor]Sumário
- RGSS3 DIY: Menu circular
- Sumário
- 1 Introdução
- 2 Estrutura geral do script
- 2.1 Scene_Menu
- 2.1.1 Ciclo de vida
- 2.1.2 Objetos da cena
- 2.1 Scene_Menu
- 3 Implementação
- 3.1 Sprite do jogador
- 3.2 Janela de dinheiro
- 3.3 Anel de comandos
- 3.3.1 CommandRing
- 3.3.2 Sprite_CommandRingItem
- 3.3.3 Spriteset
- 3.4 Controladores
- 3.5 Anel de personagens
- 3.5.1 CharacterRing
- 3.5.2 Spriteset
- 3.5.3 Sprite_CharacterRingItem
- 4 Fim!
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:
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.
[anchor=YOvXbSpnPgZVtJoe][/anchor]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.
[anchor=ZxUeoXjGPMVENeiC][/anchor]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.
[anchor=fOtjTVtTEzKSFjow][/anchor]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.
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:
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.
[anchor=MKhQlWcOaWwGDAFX][/anchor]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
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.
[anchor=HJNNUAqLuyBglSst][/anchor]3 Implementação
[anchor=BCaFsvQOMUXQeJBw][/anchor]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:
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.
Ruby:
#--------------------------------------------------------------------------
# * Inicialização do processo
#--------------------------------------------------------------------------
def start
super
create_player
end
Também é importante atualizar o sprite quando atualizamos a cena:
Ruby:
#--------------------------------------------------------------------------
# * Atualização da tela
#--------------------------------------------------------------------------
def update
super
@player.update
end
Da mesma forma, é importante dispor o sprite quando terminamos a cena:
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:
Vamos ter que tomar cuidado com isso para não desenhar as opções do menu para fora da tela.
[anchor=PKoyHeqFRWKgTdTc][/anchor]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:
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.
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:
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:
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:
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:
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.
[anchor=oBlfPrdBUGugarLv][/anchor]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.
[anchor=qgKkDcuXLWTueyFj][/anchor]3.3.1 CommandRing
Criaremos uma classe CommandRing, que representa o anel de comandos:
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.
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.
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:
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 %:
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:
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:
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:
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:
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:
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:
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.
[anchor=ospkVtkLXjwfKlds][/anchor]3.3.2 Sprite_CommandRingItem
É bem simples criar uma classe de Sprite:
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:
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:
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
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:
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:
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!)
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:
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:
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:
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.
[anchor=KsltUvkiwjfEycho][/anchor]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.
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:
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:
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:
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:
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:
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:
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:
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:
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.
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:
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:
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:
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:
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.