-
Notifications
You must be signed in to change notification settings - Fork 1
Criando um Modelo de Jogo (passo a passo)
Este documento descreve os passos necessários para criar um novo modelo de jogo a ser integrado à plataforma REMAR.
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.
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.
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.
Após a clonagem do repositório, é possível encontrar o projeto Template na pasta raiz:
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
Abra os seguintes arquivos e realize as seguintes configurações:
No arquivo application.properties, substitua o nome da aplicação. Ou seja, substitua
app.name = Template
por
app.name = Demo
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)
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)
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>
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.
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.
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).
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.
Essa seção discute as atividades necessárias à implementação da tarefa de customização do conjunto de frases.
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
}
}
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 }
}
}
}
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).
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>
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>
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>
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>
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>
Essa seção discute as atividades necessárias à implementação da tarefa de customização da imagem de fundo.
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
}
}
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 }
}
}
}
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.
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>
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>
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:
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.
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.
A figura abaixo ilustra as tarefas previstas na customização do modelo de jogo Demo.
A figura abaixo ilustra a customização do conjunto de frases a serem apresentadas.
A figura abaixo ilustra a customização da imagem de fundo a ser utilizada.
E por fim, a figura abaixo apresenta a aplicação web demo que é resultante da customização (conjunto de frases e imagem de fundo).
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.
Obrigado por ler até aqui e mãos à obra!