Last active
May 27, 2016 03:10
-
-
Save pablitar/d99487e83e0682afc466f71ea28e5fae to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import scala.util.Random | |
/** | |
* Created by pablitar on 17/05/16. | |
*/ | |
//La aplicación de escritorio sería interesante que fuera una app de scala lisa y llana | |
object PewPewApp extends VainillaApp(new PewPewGame()) | |
//Extender la superclase sería debido a que es probable que haya un ciclo de vida. | |
//Además, es un contrato fuerte que nos permite que no le compile el programa si no definió lo mínimo | |
//que necesitamos para crear un juego. gameTitle y createInitialScene serían métodos abstractos | |
class PewPewGame extends VainillaGame { | |
override def gameTitle = "Pew Pew" | |
override def createInitialScene = new PewPewGameScene(this) | |
/*No haría falta definir esto. Por defecto habría una configuración básica | |
override def configure = { | |
screenSize = (1024, 768) | |
}*/ | |
//El Game serviría para guardar "estado global" | |
def highScores = loadHighScoresFromFile() | |
} | |
//En principio, no necesitamos loaders ni nada por el estilo. Si hubiera esa necesidad, estaría bueno manejarlo a nivel | |
//de escena | |
object Resources { | |
//Muchas de las apariencias en principio serían inmutables | |
lazy val playerPhase1 = new Sprite("playerPhase1.png") | |
} | |
//Game scene también tendría su ciclo de vida. A su vez, idea interesante sería que escena sea un tipo de Game Component, | |
//Para que se puedan componer | |
class PewPewGameScene(val game: PewPewGame) extends VainillaGameScene { | |
val playerShip = new PlayerShip() | |
//Por defecto al agregar un componente lo agrego en el world space. Un espacio vendría a ser como | |
//un sistema de coordenadas. Cada espacio tiene su propia cámara. En principio, una escena tiene | |
//dos espacios, pero sería interesante permitir agregar más. | |
this.addComponents( | |
playerShip, | |
new StarFieldBackground(this.cameraFor(this.worldSpace)), | |
new EnemySpawner(this), | |
//Este componente quizás lo proveemos nosotros, o quizás lo desarrollan los alumnos. Lo que haría sería hacer | |
//que la posición de la cámara cambie con la posición de la nave. | |
new CameraFollow(playerShip, this.cameraFor(this.worldSpace)) | |
) | |
//Este en cambio lo agrego en el screen space. Esto haría que su posición sea fija en la pantalla. | |
this.addComponent(new Score(this), this.screenSpace) | |
} | |
//Ejemplo de un componente con una apariencia no estándar, y estructura no estándar | |
class StarFieldBackground(camera: Camera) extends VainillaGameScene { | |
val amountOfStars = 500 | |
//Genera estrellas posicionadas alrededor de la cámara | |
var stars:List[Star] = generateStarsFor(camera) | |
override val appearance = new StarFieldAppearance(this) | |
override def update(state: FrameState) = { | |
stars.foreach { aStar => | |
star.update(state.delta) | |
} | |
removeStarsOutOfBounds(camera) | |
addNewStarsIfNeeded(camera) | |
} | |
//Los game components definirían render y update. Update por defecto no haría nada. Render por defecto delega | |
//a la apariencia | |
} | |
class StarFieldAppearance(starFieldBackground: StarFieldBackground) extends Appearance { | |
//Esto obviamente estaría muy atado a LibGDX... hay que ver qué se provee, pero la gracia es | |
//recibir lo necesario para poder determinar si tiene que dibujarse o no, y que pueda dibujarse. | |
//La idea es que exista un trait Renderable. Los GameComponents por defecto extenderían de Renderable | |
//(o quizás no, quizás con Scala se puede hacer algo elegante en donde no todos los GameComponent extiendan de Renderable) | |
//Un Renderable permitiría obtener una posición en pantalla a partir de una cámara, y determinar si está visible o no | |
//(quizás entender bounds, o algo así, que permita determinar si está visible o no) | |
override def render(what: Renderable, camera: Camera, canvas: Canvas): Unit = { | |
//En este caso, el renderable no se usa porque se delega a las estrellas. | |
starFieldBackground.stars.foreach({ aStar => | |
aStar.render(camera, canvas) | |
}) | |
} | |
} | |
//Una estrella sería como una partícula: Las partículas tienen posición y velocidad, y quizás algo más. | |
class Star extends Renderable with Speed { | |
val size = Random.nextInt(4) + 1 | |
override def appearance = new RectangularAppearance(size, size, Color.hsb(randomHue, randomSaturation, randomBrightness)) | |
} | |
class Score(scene: PewPewGameScene) extends GameComponent { | |
var currentScore = 0 | |
//Para soportar una especie de binding, estaría bueno que los labels soporten que les pases una | |
//lambda que obtenga el texto a mostrar. Sería un parámetro by name, o quizás para hacerlo más | |
//explícito, una función, como está acá abajo | |
override val appearance = Label(() => this.scoreText) | |
def scores = scene.game.highScores | |
def scoreText = s"Score: $currentScore\nHigh Score: ${scores.highest}" | |
} | |
class PlayerShip extends GameComponent { | |
//Radianes por segundo | |
val rotationSpeed = Math.PI | |
//Unidades de distancia por segundo | |
var speed: Vector2D = (0,0) | |
//Unidades de distancia por segundo al cuadrado | |
var thrustAcceleration = 100 | |
val rotationKeys = Map( | |
Key.LEFT -> Rotation.COUNTER_CLOCKWISE, | |
Key.RIGHT -> Rotation.CLOCKWISE | |
) | |
//Todo game component debería definir una appearance. Un appearance podría o no ser un var | |
override var appearance = Resources.playerPhase1 | |
def thrustVector = { | |
//this.direction devuelve el versor director, del "frente" del objeto, en función de la matriz de transformación. | |
//También debería estar definido en GameComponent. Esto tendría el efecto de hacer que la nave acelere en la | |
//dirección en la que está mirando el jugador | |
this.thrustAcceleration * this.direction | |
} | |
//Esto es una forma propuesta, pero no necesariamente tiene que ser así. | |
override def update(state: FrameState): Unit = { | |
//Tener en cuenta que esta función parcial debería ejecutarse una vez por cada evento de input | |
state.onInput { | |
case KeyPressed(key) if rotationKeys.contains(key) => { | |
//Rotate sería un método definido por los Game Components. La idea es que todos los game components | |
//tengan una matriz de transformación lineal propia, y que los métodos rotate y parecidos encapsulen | |
//esa complejidad hasta que introduzcamos el concepto | |
this.rotate(rotationKeys(key))(state.delta * rotationSpeed) | |
} | |
case KeyPressed(Key.UP) => { | |
this.speed += this.thrustVector * state.delta | |
} | |
case KeyDown(Key.SPACE) => { | |
this.firing = true | |
} | |
case KeyUp(Key.SPACE) => { | |
this.firing = false | |
} | |
} | |
if(firing) { | |
this.cooldownAndFire() | |
} | |
//Move movería de acuerdo al vector que le pases por parámetro | |
//en el sistema de coordenadas padre, que en este caso sería global. Quizás vale la pena contemplar | |
//la posibilidad de que los game components estén anidados, y en ese caso move sería según el sistema de coordenadas | |
//del game component padre | |
//También podría ser que uno le pase un sistema de coordenadas por parámetro, que podría ser un parámetro implícito o algo así | |
this.move(this.speed * state.delta) | |
} | |
} | |
class EnemySpawner(scene: PewPewGameScene) extends GameComponent { | |
//Invisible es para los game components que no se renderizan de ninguna manera | |
override val appearance = Invisible | |
val spawnTime = 5 //segundos | |
var elapsedSinceSpawn = 0.0 | |
val spawnPositions:Seq[Vector2D] = List((0,0), (0,333), (323, 257), (111, 275)) | |
def spawnPosition = { | |
spawnPositions(Random.nextInt(spawnPositions.length)) | |
} | |
def spawnEnemy() = { | |
//Add component modifica la lista de components durante un frame. Esto implica que | |
//la iteración de los elementos tiene que hacerse en una copia de la lista. | |
//Hay que ver perfomance, pero es posible que con colecciones inmutables esto funcione bien. | |
scene.addComponent(new EnemyShip(spawnPosition)) | |
} | |
override def update(state: FrameState): Unit = { | |
elapsedSinceSpawn += state.delta | |
if(elapsedSinceSpawn > spawnTime) { | |
spawnEnemy() | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment