
Nesta parte 4, vamos falar de Scenes.
Na implementação de games complexos, algo que percebi na literatura do gênero é a adoção de scenes. Mas o que é uma scene?
Posso dizer que é um bloco de código independente que armazena dados e realiza um conjunto de funções pertinentes apenas àquela parte do jogo atual.
Mas para quê isso?
Com o uso de scenes, você pode separar cada pedaço do jogo em blocos, sendo que apenas um bloco é carregado e executado por vez.
Imagina ter que escrever toda a lógica de update de um game inteiro dentro do método Update()? Ou mesmo carregar todas as texturas do jogo inteiro no LoadContent()? Para um Asteroids, tudo bem. Mas imagina um de tamanho razoável como Braid sendo inteiramente escrito dentro de um único arquivo (Game1)?
Certamente ficaria extenso e confuso demais.
É aí que o conceito de Scene é utilizado, pois assim consegue-se separar o código e agrupar toda a lógica de cada tela/fase/cena do jogo em um único lugar, de modo que fique independente do resto e que sejam intercomunicáveis.
Por exemplo, a tela de menu tem toda uma lógica e grupo de gráficos próprios, que o resto do jogo não aproveitará. Aqui podemos agrupar tudo numa Scene. E quando o fluxo principal do jogo vai rodar pela primeira vez, a Scene é invocada e ela passa a comandar o jogo. Já no menu, ao selecionarmos o modo Arcade, a cena de menu chama a cena de jogo (ou da fase 1, dependendo do nível de agrupamento), e o menu "sai de cena".
É um conceito parecido com o teatro ou filme. Cada cena possui um grupo de falas, atuações, objetos, decoração figurantes e atc, que estão independentes do resto de tal maneira que não importa a ordem em que as cenas sejam filmadas. Depois basta rodar cada uma em ordem.
Dentro do desenvolvimento de games, uma scene geralmente é uma classe. Algumas engines já possuem uma implementação padrão para isso. Como o XNA é cru, temos que implementar nosso próprio conceito de scene.
Uma idéia que vi na literatura é criar uma classe que herda de GameComponent, pois esta classe possui todos aqueles métodos da classe Game, como Update(), LoadContent() e Draw().
A idéia com isso é que cada scene teria o poder de carregar sprites e renderizá-los sem a intervenção de Game1.
Mas quando eu fui tentar implementar isso, não consegui fazer com que o framework utilizasse os métodos corretamente. Por algum motivo, o comando Content.Load<>() usado para carregar textura e áudio não era acessível nestas classes extendidas. Ou seja, ainda não consigo carregar conteúdo fora da classe Game1.
Então eu decidi criar minha própria implementação de Scene, que considero ser menos poderosa, mas muito mais simples e fácil de onganizar, pois não depende de usar métodos obscuros do framework e de certa forma faz com que todo o processamento fique dentro de Game1, mas a implementação, fora.
Veja abaixo como funciona atualmente o fluxo das scenes no XNA Lander:
Bem, a idéia geral é que toda a lógica do jogo seja de alguma forma executada a partir do Game1, e assim tudo vai funcionar bem.
O que eu fiz foi criar uma classe Scene contendo o seguinte:
- lista de Sprites;
- lista de Fontes;
- método LoadContent();
- método Update().
Para cada scene usada no jogo, eu crio uma nova classe e herdo de Scene. Aí eu declaro todos os sprites que serão utilizados na classe e adiciono na lista. Faço o mesmo com as fontes. Mas só declaro, e não as carrego, pois fora de Game1 eu não consegui. No método LoadContent() eu carrego ou inicializo qualquer outra coisa necessária para aquela scene funcionar (por exemplo, recarregar o combustível nave para uma nova fase caso seja a Scene de jogo).
No método Update() eu escrevo toda a lógica de fluxo de jogo para a Scene. Caso seja a Scene de menu, o Update() ficará lendo as teclas para mover o cursor pelo menu, além de verificar se o jogador pressionou Enter para selecionar a opção desejada.
Com isso, eu carrego a Scene desejada num objeto de visibilidade global (usando public static) chamada de CurrentScene, de modo que Game1 possa acessá-la. Aí, no método Game1.LoadContent(), eu chamo o CurrentScene.LoadContent(), e depois eu percorro a lista de sprites e carrego as texturas. No método Game1.Update(), invoco o CurrentScene.Update(). E no método Game1.Draw(), eu percorro a lista de sprites da CurrentScene e renderizo em ordem uma a uma. Isso tudo sem o jogo saber qual scene está sendo usada no momento, pois independente de qual seja, ela estará no objeto CurrentScene. O Game1 executa os mesmos passos para qualquer scene carregada. E isso é possível porque todas elas herdam de Scene e se comportam de maneira similar. Só seus conteúdos mudam.
Com isso, toda a lógica e sprites/fontes ficam contidos em cada scene, mas seu carregamento e execução ainda ficam atrelados a Game1.
Após colocar isso em prática, basta codificar uma vez o Game1 e todo o resto do game passa a ser codificado através das Scenes.
Veja o void Initialize() do Game1:
Este código acima só rodará uma vez durante a execução de todo o game. Nada impede que você chame depois o método, entretanto. Então eu aproveito para carregar a primeira scene do jogo, que é a SceneTitle(), da tela de título e menu principal.
Vamos para o LoadContent():
Aqui no LoadContent() eu chamo o CurrentScene.LoadScene(), que pode ter qualquer carregamento que não seja gráfico.
Para carregar os sprites eu percorro a lista de sprites que carrego na Scene. Como não sei quais são os sprites a serem carregados, basta jogar todos eles na lista. Assim eu percorro ela carregando um por um. Faço o mesmo com as fontes também.
Vejamos o void Update():
Veja que aqui eu chamo o Update() que está no scene. Outra coisa que faço é verificar se devo chamar novamente o Game1.LoadContent(). Mas quando isso acontece? Simples, devo chamar sempre um LoadContent() quando carrego uma nova Scene. Assim que carrego qualquer scene, mudo um boolean Globals.MustLoadContent para true e o update volta novamente para o processo anterior, de carregamento dos sprites, para carregar os novos sprites da scene recém-carregada.
No Draw(), continua tudo tranquilo:
Veja que eu não renderizo o sprite ou font quando o Visible está como false. O resto é só renderizar cada sprite na lista da Scene carregada.
Aqui eu mostrei mais como o jogo invoca e usa as scenes, mas não expliquei a implementação de cada Scene internamente. No próximo post eu falo mais sobre isso, inclusive com exemplos de código.
Tags: Desenvolvimento, XNA, XNA Lander
Categorias: Desenvolvimento, Ferramentas | No Comments »