Front End

30 nov, 2015

React.js para jogos de texto incrementais

Publicidade

Eu tenho uma pequena queda por jogos de texto bobos como Candy Box e Dark Room e, mais recentemente, o Jogo Kittens (“A Dark Souls de jogos incrementais”). A característica comum desses jogos é que, na maioria das vezes, a interface do usuário constitui-se em apenas números em uma página web, e você deve executar determinadas ações para aumentar esses números de forma a comprar upgrades que continuem a sua capacidade de aumentar os números.

Uma das boas coisas sobre esse estilo de jogo é que a barreira de entrada é muito baixa (provavelmente é por isso que há sempre um brasileiro ao redor dessas coisas), e os jogos do gênero são diferenciados quase que exclusivamente pela qualidade da sua mecânica (sem coisas tolas como “a arte” ficando no caminho). Você, leitor, pode até estar interessado em mergulhar no mundo dos jogos incrementais. Se você quiser adicionar um à sua pilha, eu recomendo que você use React para fazê-lo.

Por que usar React para isso?

Muito se fala sobre o React ser muito rápido e outras coisas, mas isso não vai ser um grande obstáculo para qualquer framework Javascript moderno, uma vez que é provável que não há muito para renderizar. A característica que se destaca no React está em sua filosofia de automaticamente fazer uma re-renderização baseada em algum estado root. O estado de gerenciamento de recursos de jogos baseados em texto pode ser muito facilmente representado como um objeto, e não é necessário se preocupar sobre como atualizar a interface de usuário, o que simplifica muito.

Aqui, eu vou lhe mostrar.

Nosso jogo bobo

Vamos expor nosso jogo de texto. A fórmula deve ser bastante familiar:

  • Você pode clicar em um botão para obter uma lama.
  • Com 10 lamas, você pode fazer um tijolo.
  • Com 100 tijolos, você pode fazer um barraco, que produzirá lama ao longo do tempo.
  • Com 1.000 tijolos, você pode fazer uma mansão, que produzirá lama ao longo do tempo a uma taxa mais favorável.
  • Com 500 tijolos, você pode fazer uma olaria, que reduz o custo de construir barracos e mansões em 5% cada.

Eu suspeito que a maioria desses jogos comecem com uma lista curta como essa e expandam a partir daí, mas isso é o suficiente para fins de demonstração.Você pode ver como ele ficou, aqui. Então, como isso se traduz no React?

Um pequeno trecho de código

Meu invólucro preferido em torno do React é o Om. Eu gosto do Reagent também, mas cursores Om podem vir a calhar em aplicativos maiores. Este não é um desses, mas vamos usar Om de qualquer maneira.

Sim, sim, eu sei que Clojurescript não é Javascript, mas me acompanhe um pouco. O paradigma de root-app-state do React interage muito bem com os data types imutáveis ​​do Clojure, e uma vez que você provavelmente estará executando algum fluxo de trabalho de pré-processamento, conseguirá obter sua JSX em qualquer maneira.

De qualquer forma, o Om é totalmente sobre ter um único estado de aplicativo global e retorna a UI com base nele. Aqui está o estado do aplicativo com que comecei:

(defonce app-state (atom {:resources {:mud 0
                                      :bricks 0}
                          :buildings {:shacks 0
                                      :mansions 0
                                      :brickyards 0}
                          :flags #{}}))

Nossa mecânica do jogo é simples: barracos e mansões acrescentam lama à nossa contagem por segundo, e olarias ganham desconto na compra dos dois.

Nós representamos a escavação automatizada de lama em um loop evento central:

(defn tick [data]
  (-> data
      (update-in [:resources :mud] + (* (-> data :buildings :shacks) 0.01)
                                     (* (-> data :buildings :mansions ) 0.12))))

(js/setInterval #(swap! app-state tick) 20)

Tudo o que temos a fazer é aumentar a contagem de lama. Simples!

Saltando para o fim, aqui está o componente que rendriza a UI. Eu usei sablono para permitir uma sintaxe compatível com Om. Se você não tem ideia do que isso significa, basta prestar atenção na nidificação de [vectors] que representam elementos HTML e seus conteúdos.

(defn widget [data]
  (om/component
   (html
     [:div.container
      [:ul.resources
       [:li "Mud: " (-> data :resources :mud)]
       [:li "Bricks: " (-> data :resources :bricks)]
       ]
      [:ul.buildings
       [:li "Shacks: " (-> data :buildings :shacks)]
       [:li "Mansions: " (-> data :buildings :mansions)]
       [:li "Brickyards: " (-> data :buildings :brickyards)]
       ]
      [:div.controls
       (action-button data (fn [data] true) pick-mud! "Dig Mud" :mud)
       [:br]
       (purchase-button data [:resources :mud] :mud->brick [:resources :bricks] 1 "Make Brick" :1b)
       (purchase-button data [:resources :mud] :mud->brick [:resources :bricks] 10 "10" :10b)
       (purchase-button data [:resources :mud] :mud->brick [:resources :bricks] 100 "100" :100b)
       (purchase-button data [:resources :mud] :mud->brick [:resources :bricks] 1000 "1000" :1000b)
       [:br]
       (purchase-button data [:resources :bricks] :bricks->shack [:buildings :shacks] 1 "Build Shack" :1s)
       (purchase-button data [:resources :bricks] :bricks->shack [:buildings :shacks] 10 "10" :10s)
       (purchase-button data [:resources :bricks] :bricks->shack [:buildings :shacks] 100 "100" :100s)
       [:br]
       (purchase-button data [:resources :bricks] :bricks->mansion [:buildings :mansions] 1 "Build Mansion" :1m)
       (purchase-button data [:resources :bricks] :bricks->mansion [:buildings :mansions] 10 "10" :10m)
       (purchase-button data [:resources :bricks] :bricks->mansion [:buildings :mansions] 100 "100" :100m)
       [:br]
       (purchase-button data [:resources :bricks] :bricks->brickyard [:buildings :brickyards] 1 "Build Brickyard" :1by)
       (purchase-button data [:resources :bricks] :bricks->brickyard [:buildings :brickyards] 10 "10" :10by)
       (purchase-button data [:resources :bricks] :bricks->brickyard [:buildings :brickyards] 100 "100" :100by)
       ]
      ])))

(om/root widget app-state {:target js/document.body})

Não, isso não foi tão ruim.

Esse widget será re-renderizado toda vez que houver alterações nos dados de entrada (uma vez por elemento, mais sempre que alguém clica em um botão). Uma vez que ele é montado como o componente de raiz, ele recebe todo o estado do aplicativo e atualizações quando o estado mudar. Para um mapa mais amplo do estado do aplicativo, poderíamos considerar o uso de cursores para separar as coisas que mudam a cada clique, das coisas que não mudam, mas isso não é lá tão importante.

Fluxos de dados de ligação mudam inteiramente, então, quando precisamos mudar o estado do aplicativo, usamos uma função especial, chamada om/transact!, que altera o estado aplicativo e desencadeia uma re-renderização. Assim como no React, o Om tem um local próprio para elementos dom virtuais, então a re-renderização é bem menos dispendiosa do que parece.

A função purchase-button é apenas uma função que usamos para deduzir a contagem de lama e adicionar à contagem de construção adequada. Passamos algumas chaves para olhar para os números corretos referentes a isso. Aqui estão todas as funções associadas com custos e compra:

(def cost {:mud->brick (fn [_] 10)
           :bricks->shack (fn [data] (* 100 (js/Math.pow 0.95 (-> data :buildings :brickyards))))
           :bricks->mansion (fn [data] (* 1000 (js/Math.pow 0.95 (-> data :buildings :brickyards))))
           :bricks->brickyard (fn [_] 500)})

(defn check-cost [selector cost-key n]
  (fn [data]
    (>= (get-in data selector) (* n ((cost cost-key) data)))))


; Purchasing

(defn pick-mud! [data]
  (update-in data [:resources :mud] inc))

(defn buy-item! [data in-selector cost-key out-selector n]
  (-> data
      (update-in in-selector - (* n ((cost cost-key) data)))
      (update-in out-selector + n)))

(defn action-button [data enabled-fn? action-fn! button-text id]
  (let [enabled? (enabled-fn? data)
        previously-enabled? (-> data :flags id)
        ]
    (when (and enabled? (not previously-enabled?))
      (om/transact! data #(update-in % [:flags] conj id)))
    (html/submit-button {:style (if-not (or enabled? previously-enabled?) {:display "none"} {})
                         :disabled (if-not enabled? "disabled")
                         :on-click (fn [_] (om/transact! data action-fn!))}
                        button-text)))

(defn purchase-button [data in-selector cost-key out-selector n text id]
  (action-button data
                 (check-cost in-selector cost-key n)
                 #(buy-item! % in-selector cost-key out-selector n)
                 text id))

Ok, então se você não está confortável com a sintaxe lisp, esta é a parte que vai parecer bobagem para você. Para lhe poupar o trabalho, aqui estão as notas importantes sobre cada função.

  • pick-mud! é uma função one-off que só aumenta a nossa contagem de lama.
  • buy-item! é uma função que aceita o mapa do estado e alguns seletores, remove cost do recurso e acrescenta n no recurso. Você pode ver os custos no mapa custo perto do topo, juntamente com a função check-cost.
  • action-button cuida de renderizar um botão com uma determinada ação. Ele também aceita um teste para ver se a ação está disponível (um lugar útil para uma função de custo), e certifique-se de ocultar quaisquer botões que nunca foram ativados (e é para isso que os sinalizadores são definidos).
  • purchase-button apenas ativa action-button para buy-item!, cuidando dos bits comuns.

E é isso aí! Essas mais ou menos 100 linhas de compilação de código (com dependências) em um arquivo javascript podem ser colocadas em qualquer página para transformá-la em um simulador de cavar lama emocionante. Claro que se você fosse mais além nesse tipo de coisa do que eu fui aparentemente, provavelmente teria chegado mais longe na mecânica do negócio antes de ficar entediado e blogar sobre isso, então eu espero que meu trabalho tenha te inspirado a fazer isso (se você estiver a fim desse tipo de coisa)!

Mais uma vez, você pode ver como ficou em http://adambard.github.io/silly-mud-game/. Boa sorte em seus experimentos!

***

Adam Bard faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: http://adambard.com/blog/incremental-text-games/