Skip to content

Criando um Modelo de Jogo (passo a passo)

Lucas Yuji Suguinoshita Aciole edited this page Feb 28, 2018 · 44 revisions

Este documento descreve os passos necessários para criar um novo modelo de jogo a ser integrado à plataforma REMAR.

1. Setup

Instalação do Java

Caso você não tenha o Java instalado em seu ambiente, é necessário instalá-lo utlizando os comandos:

sudo add-apt-repository ppa:webupd8team/java
sudo apt-get update
sudo apt-get -y install oracle-java8-installer oracle-java8-set-default
sed -i '$ a export JAVA_HOME=/usr/lib/jvm/java-8-oracle' ~/.bashrc

Observação: A descrição dos comandos acima foi baseada na execução em uma máquina Ubuntu 16.04 LTS. Caso o leitor esteja utilizando um ambiente de desenvolvimento distinto, solicita-se que acesse o seguinte link para maiores informações sobre a instalação do Java em seu ambiente.

Instalação do Grails

Caso você não tenha o Grails instalado em seu ambiente, é necessário instalá-lo utlizando os comandos:

curl -s get.sdkman.io | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk install grails 2.4.5

Observação: A descrição dos comandos acima foi baseada na execução em uma máquina Ubuntu 16.04 LTS. Caso o leitor esteja utilizando um ambiente de desenvolvimento distinto, solicita-se que acesse o seguinte link para maiores informações sobre a instalação do Grails em seu ambiente.

Clone do repositório:

O repositório da plataforma REMAR contém um projeto Template que pode ser utilizado como base para novos modelos de jogos. Advoga-se que os desenvolvedores de novos modelos de jogos utilizem esse projeto como base.

Para isso, o primeiro passo é clonar o repositório do Projeto REMAR colocando o seguinte comando no terminal:

git clone https://github.com/LOA-SEAD/projeto-remar.git

Caso você não tenha o GIT instalado em seu ambiente, é necessário instalá-lo utlizando o comando:

sudo apt-get install git

Observação: A descrição dos comandos acima foi baseada na execução em uma máquina Ubuntu 16.04 LTS. Caso o leitor esteja utilizando um ambiente de desenvolvimento distinto, solicita-se que acesse o seguinte link para maiores informações sobre a instalação do GIT em seu ambiente.

Cópia:

Após a clonagem do repositório, é possível encontrar o projeto Template na pasta raiz:

Localização do projeto Template

Podemos copiar todo seu conteúdo para uma pasta com o nome do novo modelo (como exemplo utilizaremos Demo):

cd projeto-remar
cp -r Template Demo
rm Demo/Manual.pdf Demo/README.md

Configuração:

Abra os seguintes arquivos e realize as seguintes configurações:

application.properties

No arquivo application.properties, substitua o nome da aplicação. Ou seja, substitua

app.name = Template

por

app.name = Demo

Config.groovy

No arquivo grails-app/conf/Config.groovy, substitua o contexto da aplicação. Ou seja, substitua

grails.app.context = "/TemplateName"

por

grails.app.context = "/demo" (há duas ocorrências no arquivo) 

DataSource.groovy

No arquivo grails-app/conf/DataSource.groovy, substitua a url do acesso ao banco de dados. Ou seja, substitua

url = "jdbc:mysql://localhost/TemplateName"

por

url = "jdbc:mysql://localhost/demo" (há 3 ocorrências no arquivo)

main.gsp

No arquivo grails-app/views/layouts/main.gsp, substitua a uri da aplicação. Ou seja, substitua

<link type="text/css" rel="stylesheet" href="/TemplateName/css/materialize.min.css" media="screen,projection"/>
<script type="text/javascript" src="/TemplateName/js/materialize.min.js"></script>

por

<link type="text/css" rel="stylesheet" href="/demo/css/materialize.min.css" media="screen,projection"/>
<script type="text/javascript" src="/demo/js/materialize.min.js"></script>

env.properties

Por fim, é necessário editar o arquivo grails-app/conf/env.properties. Este é um arquivo que contém as principais variáveis de ambiente utilizadas, para a definição, por exemplo, dos acessos ao banco de dados da plataforma em questão.

dataSource_remar.username=root
dataSource.dbHost=mongodb
dataSource.password=root
dataSource_remar.password=root
dataSource.url=jdbc\:mysql\://mysql/demo
dataSource_remar.url=jdbc\:mysql\://mysql/remar
dataSource.username=root

Nele são definidos o usuário e senha do banco de dados, sua url de acordo com a porta em uso e outros dados atrelados à este aspecto de armazenamento de dados por parte da aplicação.

2. Definição dos pontos de customização

Assim, podemos agora começar a modelar o nosso exemplo, que se baseará em uma aplicação demo web em que há 2 pontos de customização: conjunto de frases e a imagem de fundo.

Customização 1

Customização 2

Observação: o código-fonte base (HTML, CSS, Javascript, etc), disponível no seguinte link, necessita ser copiado para a pasta web-app/remar/source (Ver discussão na Seção Compilação e Deploy do Modelo).

Todos os modelos de jogos integrados à plataforma REMAR devem conter um processo de customização (representado pelo arquivo process.json). Um processo é composto por diversas tarefas (tasks), que por sua vez possuem uma ou mais saídas (outputs). As saídas (outputs) representam arquivos, presentes no código-fonte do jogo, que serão substituídos durante o processo de customização. Para maiores informações sobre o conteúdo do arquivo process.json, acesse o Manual de Desenvolvedor REMAR.

As tarefas (tasks) são implementadas como controladores Grails (arquitetura MVC) e possuem a responsabilidade pela implementação das atividades de customização previstas em um jogo. Ou seja, é responsabilidade das tarefas:

  • disponibilizar uma interface web (visões associadas ao controlador) para que as informações associadas a cada customização prevista sejam informadas;

  • criar as saídas (outputs) customizadas; e

  • enviar as saídas (outputs) à plataforma REMAR para que este produza e publique uma nova instância customizada do jogo.

Dessa forma, é necessário definir, no diretório web-app/remar/, o arquivo process.json que possui a seguinte estrutura:

{
    "name": "Demo",
    "uri": "demo",
    "version": 1,
    "tasks": [
        {
            "name": "Banco de Frases",
            "uri": "phrase",
            "description": "Criar/editar/selecionar frases para o demo.",
            "type": "text_database",
            "dependencies": null,
            "outputs": [
                {
                    "name": "frases.json",
                    "type": "json",
                    "path": "json"
                }
            ]
        },
        {
            "name": "Imagem de Fundo",
            "uri": "theme",
            "description": "Criar/editar/selecionar imagem (background) para o demo.",
            "type": "image",
            "dependencies": null,
            "optional": true,
            "outputs": [
                {
                    "name": "bg.png",
                    "type": "image",
                    "path": "img"
                }
            ]
        }
    ],
    "outputs": [
        "web"
    ],
    "vars": {
        "width": 1280,
        "height": 720
    }
}

Conforme pode-se observar, o processo de customização, representado pelo arquivo process.json, é composto por duas tarefas (sendo uma opcional). Cada tarefa é responsável pela implementação das atividades de customização previstas (conjunto de frases e a imagem de fundo). As tarefas são implementadas como controladores (discutidos nas Seções PhaseController e Theme Controller).

3. Desenvolvimento da aplicação web (Arquitetura MVC)

A plataforma REMAR foi desenvolvida utilizando o framework de desenvolvimento Grails. Dessa forma, advoga-se a utilização desse framework no desenvolvimento de novos modelos de jogos.

Conforme discutido anteriormente, no contexto da aplicação demo web, há 2 atividades de customização previstas (conjunto de frases e a imagem de fundo). Discutiremos o desenvolvimento dessas atividades nessa seção.

3.1. Customização: Conjunto de frases

Essa seção discute as atividades necessárias à implementação da tarefa de customização do conjunto de frases.

3.1.1. Classes de Domínio: Phrase.groovy

Levando em consideração o padrão arquitetural MVC, as classes de domínio fazem parte do modelo (o M do MVC) de uma aplicação Grails. As classes de domínio são responsáveis pela representação dos dados gerenciados pela aplicação. Geralmente, cada classe de domínio está relacionada a uma tabela do banco de dados, onde seus atributos são concretizados em colunas da tabela.

A definição das classes de domínio é o primeiro passo do processo de desenvolvimento de uma tarefa de customização associada a um modelo de jogo na plataforma REMAR. Ela é feita a partir da reflexão de quais dados precisam ser persistidos para fazer a customizações possíveis de um jogo.

Alguns exemplos de classes domínio:

  • Classe Questão (para um jogo de perguntas)
  • Classe Peça (para um jogo de memória)
  • Classe Palavra (para um jogo que ensina ortografia)

A classe de domínio Phrase representa o conjunto de frases a serem apresentadas na aplicação demo web customizada.

package br.ufscar.sead.loa.demo.remar

class Phrase {

    String content
    String author
    long ownerId
    String taskId

    static constraints = {
        content blank: false, maxSize: 150
        author blank: false
        ownerId blank: false, nullable: false
        taskId nullable: true
    }
}

3.1.2. Controlador: PhraseController

Uma vez que a classe de domínio está criada, é preciso criar o controlador e as visões relacionados a essa classe de domínio. Na geração de tais artefatos de software será utilizada a abordagem scaffolding que é um termo adotado pelo framework Grails para a geração dos artefatos (controladores, visões, etc.) que implementam as operações CRUD.

Observação: Para cada método correspondente a uma ação em um controlador é criada uma correspondente visão (arquivo com extensão .gsp). Por exemplo, a ação show() tem o correspondente show.gsp, enquanto a ação create() tem o correspondente create.gsp.

Para criar o controlador PhraseController e as visões associadas, execute o seguinte comando:

grails generate-all br.ufscar.sead.loa.demo.remar.Phrase

Observação: O controlador PhraseController produzido pelo scaffolding foi alterado minimamente de forma a atender às características da customização de frases e/ou da plataforma REMAR (autenticação de usuários, geração do arquivo frases.json, etc). O controlador PhraseController contém as seguintes ações:

  • A ação index() é responsável por retornar a lista de instâncias. Essa lista é repassada para visão index.gsp que a apresenta em uma página HTML;

  • A ação show() é responsável por retornar os atributos de uma instância. Essa instância é repassada para a visão show.gsp que a apresenta em uma página HTML;

  • A ação create() é responsável por criar uma instância que é repassada (retornada) para a visão create.gsp (uma página que contém um formulário HTML);

  • Quando o formulário é submetido, a ação save() valida os dados e, caso tenha sucesso, grava a instância no banco de dados e redireciona para a ação index(). Por outro lado, se os dados são inválidos, a ação save() renderiza a visão create.gsp novamente para que o usuário corrija os erros encontrados na validação;

  • A ação edit() é responsável por recuperar uma instância a ser atualizada posteriormente. A instância recuperada é repassada (retornada) para a visão edit.gsp (uma página que contém um formulário HTML);

  • Quanto o formulário é submetido o método update() valida os dados e, caso tenha sucesso, atualiza a instância no banco de dados e redireciona para a ação index(). Por outro lado, se os dados são inválidos, a ação update() renderiza a visão edit.gsp novamente para que o usuário corrija os erros encontrados na validação;

  • A ação delete() remove uma instância do banco de dados através da invocação do método delete (flush:true) no objeto a ser removido; e

  • Por fim, a ação toJson() cria o arquivo frases.json (saída da tarefa) a partir das frases selecionadas e finaliza a tarefa (task) responsável pela atividade de customização do conjunto de frases.

package br.ufscar.sead.loa.demo.remar

import grails.plugin.springsecurity.annotation.Secured
import static org.springframework.http.HttpStatus.*
import grails.transaction.Transactional
import groovy.json.JsonBuilder
import br.ufscar.sead.loa.remar.api.MongoHelper
import grails.util.Environment

@Secured(["isAuthenticated()"])
@Transactional(readOnly = true)
class PhraseController {

    static allowedMethods = [save: "POST", update: "PUT", delete: "DELETE"]

    def springSecurityService

    def index(Integer max) {

        if (params.t) {
            session.taskId = params.t
        }
        session.user = springSecurityService.currentUser

        def list = Phrase.findAllByAuthor(session.user.username)

        render view: "index", model: [phraseInstanceList: list, phraseInstanceCount: list.size()]
    }

    def show(Phrase phraseInstance) {
        respond phraseInstance
    }

    def create() {
        respond new Phrase(params)
    }

    @Transactional
    def save(Phrase phraseInstance) {
        if (phraseInstance == null) {
            notFound()
            return
        }

        Phrase phrase = new Phrase();
        phrase.id = phraseInstance.id
        phrase.content = phraseInstance.content

        if (phraseInstance.author == null) {
            phraseInstance.author = session.user.username
        }

        phrase.author = phraseInstance.author
        phrase.taskId = session.taskId as String
        phrase.ownerId = session.user.id

        if (phrase.hasErrors()) {
            respond phrase.errors, view:'create'
            return
        }

        phrase.save flush:true

        redirect(action: 'index')
    }

    def edit(Phrase phraseInstance) {
        respond phraseInstance
    }

    @Transactional
    def update(Phrase phraseInstance) {
        if (phraseInstance == null) {
            notFound()
            return
        }

        if (phraseInstance.hasErrors()) {
            respond phraseInstance.errors, view:'edit'
            return
        }

        phraseInstance.save flush:true

        redirect(action: 'index')
    }

    @Transactional
    def delete(Phrase phraseInstance) {

        if (phraseInstance == null) {
            notFound()
            return
        }

        phraseInstance.delete flush:true

        request.withFormat {
            form multipartForm {
                flash.message = message(code: 'default.deleted.message', args: [message(code: 'Phrase.label', default: 'Phrase'), phraseInstance.id])
                redirect action:"index", method:"GET"
            }
            '*'{ render status: NO_CONTENT }
        }
    }

    def toJson() {
        def list = Phrase.getAll(params.id ? params.id.split(',').toList() : null)
        def builder = new JsonBuilder()
        def json = builder(
                list.collect { p ->
                    ["frase"   : p.getContent(),
                     "autor"   : p.getAuthor()]
                }
        )

        log.debug builder.toString()

        def dataPath = servletContext.getRealPath("/data")
        def userPath = new File(dataPath, "/" + springSecurityService.getCurrentUser().getId() + "/" + session.taskId)
        userPath.mkdirs()

        def fileName = "frases.json"
        File file = new File("$userPath/$fileName");
        PrintWriter pw = new PrintWriter(file);
        pw.write('{ "frases":' + builder.toString() + '}');
        pw.close();

        String id = MongoHelper.putFile(file.absolutePath)

        def port = request.serverPort
        if (Environment.current == Environment.DEVELOPMENT) {
            port = 8080
        }

        redirect uri: "http://${request.serverName}:${port}/process/task/complete/${session.taskId}", params: [files: id]
    }

    protected void notFound() {
        request.withFormat {
            form multipartForm {
                flash.message = message(code: 'default.not.found.message', args: [message(code: 'phrase.label', default: 'Phrase'), params.id])
                redirect action: "index", method: "GET"
            }
            '*'{ render status: NOT_FOUND }
        }
    }
}

3.1.3. Visões

As visões produzidas pelo scaffolding também foram alteradas minimamente de forma a atender às características da customização de frases e/ou da plataforma REMAR (autenticação de usuários, geração do arquivo frases.json, etc).

phrase/index.gsp

A visão index.gsp apresenta uma lista de frases cadastradas (instâncias da classe de Domínio Phrase). A principal alteração nessa visão (em relação ao scaffolding) encontra-se relacionada à inserção do código JavaScript responsável pela seleção do conjunto de frases e submissão para a ação toJson() do controlador PhraseController (ver discussão anterior sobre esse controlador e suas ações).

<%@ page import="br.ufscar.sead.loa.demo.remar.Phrase" %>
<!DOCTYPE html>
<html>
<head>
	<meta name="layout" content="main">
	<g:set var="entityName" value="${message(code: 'phrase.label', default: 'Phrase')}"/>
	<title><g:message code="default.list.label" args="[entityName]"/></title>
</head>

<body>
<div class="nav" role="navigation">
	<ul>
		<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]"/></g:link></li>
	</ul>
</div>

<div id="list-phrase" class="content scaffold-list" role="main">
	<h1><g:message code="default.list.label" args="[entityName]"/></h1>
	<g:if test="${flash.message}">
		<div class="message" role="status">${flash.message}</div>
	</g:if>
	<table id="generalTable">
		<thead>
		<tr>
			<th><input type="checkbox" class="filled-in interno" id="checkAll"/><label for="checkAll"></label></th>
			<th>Frase</th>
		</tr>
		</thead>
		<tbody>
		<g:each in="${phraseInstanceList}" status="i" var="phraseInstance">
			<tr class="${(i % 2) == 0 ? 'even' : 'odd'}" data-id="${phraseInstance.id}">
				<td><input type="checkbox" class="filled-in" id="filled-in-box-${phraseInstance.id}"/><label for="filled-in-box-${phraseInstance.id}"></label></td>
				<td><g:link action="show" id="${phraseInstance.id}">${fieldValue(bean: phraseInstance, field: "content")}</g:link></td>
			</tr>
		</g:each>
		</tbody>
	</table>

	<a class="waves-effect waves-light btn" id="save">Enviar</a>

	<div class="pagination">
		<g:paginate total="${phraseInstanceCount ?: 0}"/>
	</div>
</div>

<script type="text/javascript" src="https://code.jquery.com/jquery-2.1.1.min.js"></script>

<script>
    $("#checkAll").click(function () {
        $('input:checkbox').not(this).prop('checked', this.checked);
    });

    $('#save').click(function () {
        var params = "";

        var trs = $("#generalTable").children('tbody').children('tr');

        $('input:checkbox:checked').each(function () {
            params += $(this).closest("tr").attr('data-id') + ',';
        });

        if (params.length) {
            params = params.substr(0, params.length - 1);
            window.top.location.href = "/demo/phrase/toJson/" + params;
        }
        else {
            alert("Selecione pelo menos uma para enviar")
        }

    });
</script>
</body>
</html>

phrase/_form.gsp

O template _form.gsp, utilizado tanto pela visão create.gsp quanto pela visão edit.gsp, representa os campos que devem ser preenchidos/alterados durante a criação/edição de instâncias da classe de domínio Phrase. A principal alteração nessa visão (em relação ao scaffolding) encontra-se relacionada à remoção de alguns campos (ownerId e taskId). Esses campos não precisam ser informados no formulário (são obtidos automaticamente).

<%@ page import="br.ufscar.sead.loa.demo.remar.Phrase" %>

<div class="fieldcontain ${hasErrors(bean: phraseInstance, field: 'content', 'error')} required">
	<label for="content">
		<g:message code="phrase.content.label" default="Content" />
		<span class="required-indicator">*</span>
	</label>
	<g:textField name="content" maxlength="150" required="" value="${phraseInstance?.content}"/>

</div>

phrase/create.gsp

A visão create.gsp é responsável pela renderização de uma página que contém um formulário HTML em que novas frases podem ser cadastradas. A principal alteração nessa visão (em relação ao scaffolding) encontra-se relacionada à remoção de alguns links desnecessários.

<!DOCTYPE html>
<html>
	<head>
		<meta name="layout" content="main">
		<g:set var="entityName" value="${message(code: 'phrase.label', default: 'Phrase')}" />
		<title><g:message code="default.create.label" args="[entityName]" /></title>
	</head>
	<body>
		<div class="nav" role="navigation">
			<ul>
				<li><g:link class="list" action="index"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
			</ul>
		</div>
		<div id="create-phrase" class="content scaffold-create" role="main">
			<h1><g:message code="default.create.label" args="[entityName]" /></h1>
			<g:if test="${flash.message}">
			<div class="message" role="status">${flash.message}</div>
			</g:if>
			<g:hasErrors bean="${phraseInstance}">
			<ul class="errors" role="alert">
				<g:eachError bean="${phraseInstance}" var="error">
				<li <g:if test="${error in org.springframework.validation.FieldError}">data-field-id="${error.field}"</g:if>><g:message error="${error}"/></li>
				</g:eachError>
			</ul>
			</g:hasErrors>
			<g:form url="[resource:phraseInstance, action:'save']" >
				<fieldset class="form">
					<g:render template="form"/>
				</fieldset>
				<fieldset class="buttons">
					<g:submitButton name="create" class="save" value="${message(code: 'default.button.create.label', default: 'Create')}" />
				</fieldset>
			</g:form>
		</div>
	</body>
</html>

phrase/edit.gsp

A visão edit.gsp é responsável pela renderização de uma página que contém um formulário HTML em que frases cadastradas podem ser editadas. A principal alteração nessa visão (em relação ao scaffolding) encontra-se relacionada à remoção de alguns links desnecessários.

<%@ page import="br.ufscar.sead.loa.demo.remar.Phrase" %>
<!DOCTYPE html>
<html>
	<head>
		<meta name="layout" content="main">
		<g:set var="entityName" value="${message(code: 'phrase.label', default: 'Phrase')}" />
		<title><g:message code="default.edit.label" args="[entityName]" /></title>
	</head>
	<body>
		<div class="nav" role="navigation">
			<ul>
				<li><g:link class="list" action="index"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
			</ul>
		</div>
		<div id="edit-phrase" class="content scaffold-edit" role="main">
			<h1><g:message code="default.edit.label" args="[entityName]" /></h1>
			<g:if test="${flash.message}">
			<div class="message" role="status">${flash.message}</div>
			</g:if>
			<g:hasErrors bean="${phraseInstance}">
			<ul class="errors" role="alert">
				<g:eachError bean="${phraseInstance}" var="error">
				<li <g:if test="${error in org.springframework.validation.FieldError}">data-field-id="${error.field}"</g:if>><g:message error="${error}"/></li>
				</g:eachError>
			</ul>
			</g:hasErrors>
			<g:form url="[resource:phraseInstance, action:'update']" method="PUT" >
				<g:hiddenField name="version" value="${phraseInstance?.version}" />
				<fieldset class="form">
					<g:render template="form"/>
				</fieldset>
				<fieldset class="buttons">
					<g:actionSubmit class="save" action="update" value="${message(code: 'default.button.update.label', default: 'Update')}" />
				</fieldset>
			</g:form>
		</div>
	</body>
</html>

phrase/show.gsp

A visão show.gsp é responsável pela renderização de uma página HTML que apresenta os atributos (campos) de uma instância da classe de domínio Phrase. A principal alteração nessa visão (em relação ao scaffolding) encontra-se relacionada à remoção de alguns campos (ownerId e taskId). Esses campos são obtidos automaticamente e portanto não tem necessidade de apresentá-los nessa visão.

<%@ page import="br.ufscar.sead.loa.demo.remar.Phrase" %>
<!DOCTYPE html>
<html>
	<head>
		<meta name="layout" content="main">
		<g:set var="entityName" value="${message(code: 'phrase.label', default: 'Phrase')}" />
		<title><g:message code="default.show.label" args="[entityName]" /></title>
	</head>
	<body>
		<div class="nav" role="navigation">
			<ul>
				<li><g:link class="list" action="index"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
			</ul>
		</div>
		<div id="show-phrase" class="content scaffold-show" role="main">
			<h1><g:message code="default.show.label" args="[entityName]" /></h1>
			<g:if test="${flash.message}">
			<div class="message" role="status">${flash.message}</div>
			</g:if>
			<ol class="property-list phrase">
			
				<g:if test="${phraseInstance?.content}">
				<li class="fieldcontain">
					<span id="content-label" class="property-label"><g:message code="phrase.content.label" default="Content" /></span>
					
						<span class="property-value" aria-labelledby="content-label"><g:fieldValue bean="${phraseInstance}" field="content"/></span>
					
				</li>
				</g:if>
			
			</ol>
			<g:form url="[resource:phraseInstance, action:'delete']" method="DELETE">
				<fieldset class="buttons">
					<g:link class="edit" action="edit" resource="${phraseInstance}"><g:message code="default.button.edit.label" default="Edit" /></g:link>
					<g:actionSubmit class="delete" action="delete" value="${message(code: 'default.button.delete.label', default: 'Delete')}" onclick="return confirm('${message(code: 'default.button.delete.confirm.message', default: 'Are you sure?')}');" />
				</fieldset>
			</g:form>
		</div>
	</body>
</html>

3.2. Customização: Imagem de fundo

Essa seção discute as atividades necessárias à implementação da tarefa de customização da imagem de fundo.

3.2.1. Classes de Domínio: Theme.groovy

Theme.groovy

A classe de domínio Theme representa a imagem de fundo a ser apresentada na aplicação demo web customizada.

package br.ufscar.sead.loa.demo.remar

class Theme {

    long ownerId

    static constraints = {
        ownerId blank: false, nullable: false
    }
}

3.2.2. Controlador: ThemeController

De maneira análoga à classe de domínio Phrase, utilizaremos abordagem scaffolding na criação do controlador e as visões associados a classe de domínio Theme.

Para criar o controlador ThemeController e as visões associadas, execute o seguinte comando:

grails generate-all br.ufscar.sead.loa.demo.remar.Theme

Observação: O controlador ThemeController produzido pelo scaffolding foi alterado de forma a atender às características da customização da imagem de fundo e/ou da plataforma REMAR (autenticação de usuários, geração do arquivo bg.png, etc).

Em especial, decidiu-se que as operações de edição (ações edit() e update()), remoção (ação delete()) e apresentação (ação show()) não são necessárias e portanto as respectivas ações não serão implementadas.

  • A ação index() é responsável por retornar a lista de instâncias. Essa lista é repassada para visão index.gsp que a apresenta em uma página HTML;

  • A ação create() é responsável por criar uma instância que é repassada (retornada) para a visão create.gsp (uma página que contém um formulário HTML);

  • Quando o formulário é submetido, a ação save() valida os dados e, caso tenha sucesso, grava a instância no banco de dados e redireciona para a ação index(). Por outro lado, se os dados são inválidos, a ação save() renderiza a visão create.gsp novamente para que o usuário corriga os erros encontrados na validação. É importante mencionar que a ação save() recebe um arquivo uploaded e salva esse arquivo em um diretório.

  • Por fim, a ação finish() cria o arquivo bg.png (saída da tarefa) a partir da imagem selecionada e finaliza a tarefa (task) responsável pela atividade de customização da imagem de fundo.

package br.ufscar.sead.loa.demo.remar

import grails.plugin.springsecurity.annotation.Secured
import static org.springframework.http.HttpStatus.*
import grails.transaction.Transactional

import br.ufscar.sead.loa.remar.api.MongoHelper
import grails.util.Environment

@Secured(["isAuthenticated()"])
@Transactional(readOnly = true)
class ThemeController {

    static allowedMethods = [save: "POST", update: "PUT", delete: "DELETE"]

    def springSecurityService

    def index(Integer max) {

        if (params.t) {
            session.taskId = params.t
        }
        session.user = springSecurityService.currentUser

        def list = Theme.findAllByOwnerId(session.user.id)

        render view: "index", model: [themeInstanceList: list, themeInstanceCount: list.size()]
    }

    def create() {
        respond new Theme(params)
    }

    @Transactional
    def save(Theme themeInstance) {
        if (themeInstance == null) {
            notFound()
            return
        }

        def userId = session.user.getId()

        def theme = new Theme()

        theme.ownerId = userId

        theme.save flush: true

        if (theme.hasErrors()) {
            respond theme.errors, view:'create'
            return
        }

        def dataPath = servletContext.getRealPath("/data")
        def userPath = new File(dataPath, "/" + userId + "/themes/" + theme.getId())
        userPath.mkdirs()

        def backgroundUploaded = request.getFile('background')

        if(!backgroundUploaded.isEmpty()) {
            def originalBackgroundUploaded = new File("$userPath/bg.png")
            backgroundUploaded.transferTo(originalBackgroundUploaded)
        }

        redirect action: "index"
    }

    def finish() {
        def theme = Theme.findById(params.id);
        def folder = servletContext.getRealPath("/data/${theme.ownerId}/themes/${theme.id}")
        def id = MongoHelper.putFile(folder + '/bg.png')

        def port = request.serverPort
        if (Environment.current == Environment.DEVELOPMENT) {
            port = 8080
        }

        redirect uri: "http://${request.serverName}:${port}/process/task/complete/${session.taskId}" +
                "?files=${id}"
    }

    protected void notFound() {
        request.withFormat {
            form multipartForm {
                flash.message = message(code: 'default.not.found.message', args: [message(code: 'theme.label', default: 'Theme'), params.id])
                redirect action: "index", method: "GET"
            }
            '*'{ render status: NOT_FOUND }
        }
    }
}

3.2.3. Visões

As visões produzidas pelo scaffolding também foram alteradas de forma a atender às características da customização da imagem de fundo e/ou da plataforma REMAR (autenticação de usuários, geração do arquivo bg.png, etc).

Observação: Desde que decidiu-se que as ações edit(), e show() do ThemeController não serão implementadas, as seguintes visões não são mais necessárias: _form.gsp e edit.gsp e show.gsp.

theme/index.gsp

A visão index.gsp apresenta uma lista de temas (imagens de fundo) cadastrados. A principal alteração nessa visão (em relação ao scaffolding) encontra-se relacionada à inserção do código JavaScript responsável pela seleção da imagem de fundo e submissão para a ação finish() do controlador ThemeController (ver discussão anterior sobre esse controlador e suas ações).

<%@ page import="br.ufscar.sead.loa.demo.remar.Theme" %>
<!DOCTYPE html>
<html>
	<head>
		<meta name="layout" content="main">
		<g:set var="entityName" value="${message(code: 'theme.label', default: 'Theme')}" />
		<title><g:message code="default.list.label" args="[entityName]" /></title>
	</head>
	<body>
		<div class="nav" role="navigation">
			<ul>
				<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
			</ul>
		</div>
		<div id="list-theme" class="content scaffold-list" role="main">
			<h1><g:message code="default.list.label" args="[entityName]" /></h1>
			<g:if test="${flash.message}">
				<div class="message" role="status">${flash.message}</div>
			</g:if>
			<table>
			<thead>
					<tr>
						<td>ID</td>
						<td>Imagem</td>
						<td>Selecionar</td>
					</tr>
				</thead>
				<tbody>
				<g:each in="${themeInstanceList}" status="i" var="themeInstance">
					<tr data-id="${themeInstance.id}" class="${(i % 2) == 0 ? 'even' : 'odd'}">
						<td>${themeInstance.id}</td>
						<td><img src="/demo/data/${fieldValue(bean: themeInstance, field: "ownerId")}/themes/${fieldValue(bean: themeInstance, field: "id")}/bg.png" alt="" height="60px" width="60px"></td>
						<td>
							<a class="waves-effect waves-light btn btn-select">X</a>
						</td>
					</tr>
				</g:each>
				</tbody>
			</table>
			<div class="pagination">
				<g:paginate total="${themeInstanceCount ?: 0}" />
			</div>
		</div>
		<script type="text/javascript" src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
	<script>
		$(".btn-select").click(function(){
		    var id = $(this).closest("tr").attr("data-id");
			window.top.location.href = "/demo/theme/finish/" + id;
		})
	</script>
	</body>
</html>

theme/create.gsp

A visão create.gsp é responsável pela renderização de uma página que contém um formulário HTML em que novas imagens de fundo podem ser cadastradas. A principal alteração nessa visão (em relação ao scaffolding) encontra-se relacionada à utilização da tag <g:uploadForm> que permite que arquivos sejam submetidos em um formulário HTML.

<!DOCTYPE html>
<html>
	<head>
		<meta name="layout" content="main">
		<g:set var="entityName" value="${message(code: 'theme.label', default: 'Theme')}" />
		<title><g:message code="default.create.label" args="[entityName]" /></title>
	</head>
	<body>
		<div class="nav" role="navigation">
			<ul>
				<li><g:link class="list" action="index"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
			</ul>
		</div>
		<div id="create-theme" class="content scaffold-create" role="main">
			<h1><g:message code="default.create.label" args="[entityName]" /></h1>
			<g:if test="${flash.message}">
			<div class="message" role="status">${flash.message}</div>
			</g:if>
			<g:hasErrors bean="${themeInstance}">
			<ul class="errors" role="alert">
				<g:eachError bean="${themeInstance}" var="error">
				<li <g:if test="${error in org.springframework.validation.FieldError}">data-field-id="${error.field}"</g:if>><g:message error="${error}"/></li>
				</g:eachError>
			</ul>
			</g:hasErrors>
			<g:uploadForm url="[resource:themeInstance, action:'save']" >
				<input type="file" name="background" />
				<fieldset class="buttons">
					<g:submitButton name="create" class="save" value="${message(code: 'default.button.create.label', default: 'Create')}" />
				</fieldset>
			</g:uploadForm>
		</div>
	</body>
</html>

4. Compilação e Deploy do Modelo

Todos os modelos de jogos, a serem submetidos à plataforma REMAR, devem ser compilados em um arquivo war (do inglês Web Application Resource ou Web Application ARchive) que é um arquivo usado para distribuir uma coleção de recursos que, juntos, constituem uma aplicação web.

Para criar um arquivo war em um projeto Grails, execute o seguinte comando:

grails war

Esse arquivo war contém uma aplicação web responsável pelas atividades relacionadas às customizações do jogo e deve possuir um diretório denominado remar que reflete os arquivos encontrados na pasta web-app/remar do projeto Grails que contém alguns itens essenciais para o modelo de jogo ser aceito na plataforma REMAR:

  • o arquivo process.json, que foi discutido anteriormente;

  • o diretório source que contém o código executável do jogo a ser customizado;

  • o diretório images que contém o arquivo do banner do jogo que será utilizado na plataforma REMAR. O formato do nome desse arquivo é <url-do-jogo>-banner.png. No caso de nosso exemplo, o nome do arquivo seria demo-banner.png.

Caso deseje, pode utilizar essa imagem: Demo Banner


Para incorporar o modelo de jogo na plataforma REMAR, basta dar upload do arquivo war na área de desenvolvedor da plataforma REMAR. Para maiores informações sobre a compilação e deploy de modelo de jogos, acesse o Manual de Desenvolvedor REMAR.


Upload WAR

5. Customização do Modelo

Após o upload, o jogo ficará sujeito à aprovação dos administradores da plataforma. Caso aprovado, o novo modelo de jogo estará disponível para utilização pelos usuários da plataforma REMAR.


Customização


A figura abaixo ilustra as tarefas previstas na customização do modelo de jogo Demo.


Atividades de Customização


A figura abaixo ilustra a customização do conjunto de frases a serem apresentadas.


Customização - Frases


A figura abaixo ilustra a customização da imagem de fundo a ser utilizada.


Customização - Imagem


E por fim, a figura abaixo apresenta a aplicação web demo que é resultante da customização (conjunto de frases e imagem de fundo).


Customização - app

6. Considerações finais

Parabéns!! Agora você já sabe como criar as páginas de customização para o seu jogo e já pode integrá-lo à plataforma REMAR.

Lembre-se que esses são os passos básicos da criação da customização de um exemplo simples (conjunto de frases e imagem de fundo). Para conferir exemplos mais completos, sugere-se que acesse os projetos/diretórios (modelos de jogos) presentes no repositório do Projeto REMAR.

Localização do projeto Template

Obrigado por ler até aqui e mãos à obra!