├── .gitignore ├── README.md ├── src └── main │ └── scala │ └── game │ ├── Driver.scala │ ├── MultiplayerServer.scala │ ├── behaviour │ └── Simulate.scala │ ├── MultiplayerClient.scala │ ├── render │ └── Render.scala │ ├── state │ └── State.scala │ ├── SinglePlayerGame.scala │ └── event │ └── Event.scala └── game_example.sbt /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | game_example 2 | ============ 3 | 4 | Game events example (for talk) 5 | -------------------------------------------------------------------------------- /src/main/scala/game/Driver.scala: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import game.state._ 4 | 5 | object Driver extends App { 6 | val world = { 7 | val w = new World(size = Vec2i(16, 16)) 8 | w.entities += 1 -> new Living(Vec2i(1, 1)) 9 | w.entities += 2 -> new Item(Vec2i(10, 10), Item.sword) 10 | World.assignBorders(w, 1) 11 | w 12 | } 13 | 14 | SinglePlayerGame.run(world, timesteps = 1) 15 | } -------------------------------------------------------------------------------- /src/main/scala/game/MultiplayerServer.scala: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import game.behaviour._ 4 | import game.render._ 5 | import game.state._ 6 | import game.event._ 7 | 8 | abstract class ClientConnection { 9 | // send 10 | def sendPlayerEvents(event:Seq[Event], stateTransitions:Seq[Event]) 11 | 12 | // receive 13 | def receive : (Seq[Event], Seq[StateTransition[Living]]) 14 | } 15 | 16 | object MultiplayerServer { // server is arg 17 | def run(world:World, timesteps:Int, clients:Map[Int, ClientConnection]) { 18 | for(i <- 1 to timesteps) { 19 | 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/scala/game/behaviour/Simulate.scala: -------------------------------------------------------------------------------- 1 | package game.behaviour 2 | 3 | import game.state._ 4 | import game.event._ 5 | 6 | object Simulate { 7 | // occurs at an instant, generally AI or player controls 8 | def decide(world:World, l:Living) : DecisionOutput = { 9 | 10 | // ai goes here 11 | 12 | DecisionOutput(Seq.empty, Seq.empty) 13 | } 14 | 15 | // occurs over a period of time 16 | def simulate(world:World, e:Entity) : SimulationOutput = { 17 | // move entity 18 | 19 | // animations etc. would go here (as collisions can depend on them) 20 | 21 | // check for collisions 22 | 23 | SimulationOutput(Seq.empty, Seq.empty) 24 | } 25 | } -------------------------------------------------------------------------------- /src/main/scala/game/MultiplayerClient.scala: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import game.behaviour._ 4 | import game.render._ 5 | import game.state._ 6 | import game.event._ 7 | 8 | abstract class ServerConnection { 9 | // send 10 | def playerUpdate(events:Seq[Event], stateTransitions:Seq[StateTransition[Living]]) 11 | 12 | // receive 13 | def receivePlayer : (Seq[ToEntityEvent], Seq[StateTransition[Living]]) 14 | def receiveNpcStateTransitions : Map[Int, Seq[StateTransition[Living]]] 15 | } 16 | 17 | object MultiplayerClient { // server is arg 18 | def run(world:World, timesteps:Int, server:ServerConnection) { 19 | for(i <- 1 to timesteps) { 20 | 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/main/scala/game/render/Render.scala: -------------------------------------------------------------------------------- 1 | package game.render 2 | 3 | import game.state.World 4 | 5 | object Render { 6 | def render(world:World, step:Int) { 7 | (1 to 5).foreach { _ => println("") } 8 | 9 | val drawBuffer = world.blocks.map { b => 10 | b match { 11 | case 0 => ' ' 12 | case 1 => (0x2593).asInstanceOf[Char] 13 | case _ => '.' 14 | } 15 | } 16 | 17 | // applies entity to drawbuffer 18 | for(entity <- world.entities.values) { 19 | val i = world.size.y * entity.position.x + entity.position.y 20 | drawBuffer(i) = entity.glyph 21 | } 22 | 23 | println(s"== Step $step ==") 24 | for(line <- drawBuffer.grouped(world.size.x)) { 25 | println(line.mkString(" ")) 26 | } 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /game_example.sbt: -------------------------------------------------------------------------------- 1 | name := "game example" 2 | 3 | version := "1.0" 4 | 5 | organization := "Michael Shaw" 6 | 7 | compileOrder := CompileOrder.JavaThenScala 8 | 9 | scalaVersion := "2.10.0" 10 | 11 | scalacOptions ++= Seq("-unchecked", "-deprecation") // , "-optimise" 12 | 13 | javaOptions ++= Seq("-Xmx1G","-Xms1G", "-server") 14 | 15 | javaOptions ++= Seq("-XX:+AggressiveOpts") 16 | 17 | javaOptions ++= Seq("-Xdebug", "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005") 18 | 19 | resolvers += "Typesafe Repository" at "http://repo.typesafe.com/typesafe/releases/" 20 | 21 | resolvers += "Central" at "http://repo2.maven.org/maven2" 22 | 23 | resolvers += "Guicey" at "http://guiceyfruit.googlecode.com/svn/repo/releases/" 24 | 25 | resolvers += Resolver.sonatypeRepo("snapshots") 26 | 27 | fork := true 28 | 29 | retrieveManaged := true -------------------------------------------------------------------------------- /src/main/scala/game/state/State.scala: -------------------------------------------------------------------------------- 1 | package game.state 2 | 3 | import collection.mutable 4 | 5 | 6 | class World(val size:Vec2i) { 7 | val blocks = new Array[Byte](size.product) 8 | val entities = new mutable.HashMap[Int, Entity]() 9 | 10 | def hasBlockAt(x:Int, y:Int) = getBlockAt(x, y) > 0 11 | 12 | def getBlockAt(x:Int, y:Int) : Byte = { 13 | blocks(blockPosition(x, y)) 14 | } 15 | 16 | def setBlockAt(x:Int, y:Int, b:Byte) { 17 | blocks(blockPosition(x, y)) = b 18 | } 19 | 20 | private def blockPosition(x:Int, y:Int) : Int = { 21 | size.y * x + y 22 | } 23 | } 24 | 25 | object World { 26 | def takeDamage(damage:Int)(l:Living) : Living = { 27 | l.copy(health = floor(l.health - damage, 0, 100)) 28 | } 29 | 30 | def floor(n:Int, min:Int, max:Int) = { 31 | if(n < min) { 32 | min 33 | } else if (n > max) { 34 | max 35 | } else { 36 | n 37 | } 38 | } 39 | 40 | 41 | def assignBorders(world:World, b:Byte) { 42 | for { 43 | x <- 0 until world.size.x 44 | } { 45 | world.setBlockAt(x, 0 , b) 46 | world.setBlockAt(x, world.size.y - 1, b) 47 | } 48 | 49 | for { 50 | y <- 0 until world.size.y 51 | } { 52 | world.setBlockAt(0, y, b) 53 | world.setBlockAt(world.size.x - 1, y, b) 54 | } 55 | } 56 | } 57 | 58 | abstract class Entity(val position:Vec2i) { 59 | var alive = true // needed to track an object being picked up while still processing all of it's events 60 | 61 | val events = new mutable.Queue[game.event.ToEntityEvent]() 62 | 63 | def glyph : Char 64 | } 65 | 66 | case class Living(pos:Vec2i, var health:Int = 100) extends Entity(pos) { 67 | val facing = Vec2i(1, 0) 68 | var weaponDrawn = false 69 | var attacking = false 70 | 71 | var itemInHand : Option[Int] = None 72 | 73 | def glyph = 0x263a 74 | 75 | def holdingItem = itemInHand.isDefined 76 | 77 | 78 | 79 | 80 | } 81 | 82 | class Item(pos:Vec2i, val itemType:Int) extends Entity(pos) { 83 | def glyph = 0x2663 84 | 85 | } 86 | 87 | object Item { 88 | val sword = 0 89 | val crate = 1 90 | } 91 | 92 | case class Vec2i(var x:Int, var y:Int) { 93 | def product = x * y 94 | def :=(other:Vec2i) { 95 | x = other.x; 96 | y = other.y 97 | } 98 | } -------------------------------------------------------------------------------- /src/main/scala/game/SinglePlayerGame.scala: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import game.behaviour._ 4 | import game.render._ 5 | import game.state._ 6 | import game.event._ 7 | 8 | import collection.mutable 9 | 10 | object SinglePlayerGame { 11 | def run(world:World, timesteps:Int) { 12 | for(i <- 1 to timesteps) { 13 | update(world) 14 | Render.render(world, i) 15 | } 16 | } 17 | 18 | def update(world:World) { 19 | def routeEntityEvent(tee:ToEntityEvent) { 20 | world.entities(tee.to).events += tee 21 | } 22 | 23 | val events = (for((id, e) <- world.entities) yield { 24 | e match { 25 | case living:Living => updateLiving(world, living, id) 26 | case item:Item => updateItem(world, item, id) 27 | } 28 | }).flatten // accumulate events and route them out 29 | 30 | val zoneEvents = new mutable.ArrayBuffer[ZoneEvent]() 31 | 32 | for(event <- events) { 33 | event match { 34 | case tee:ToEntityEvent => routeEntityEvent(tee) 35 | case ze:ZoneEvent => zoneEvents += ze 36 | } 37 | } 38 | 39 | // mutex over the zone events 40 | } 41 | 42 | def updateLiving(world:World, living:Living, id:Int) : Seq[Event] = { 43 | val replyEvents = living.events.flatMap { ev => 44 | applyLivingEvent(living, ev, id) 45 | } 46 | 47 | val decision = Simulate.decide(world, living) 48 | 49 | applyStateTransitionToLiving(living, decision.transitions) 50 | 51 | // apply decision events 52 | val simulate = Simulate.simulate(world, living) 53 | 54 | applyStateTransitionToLiving(living, simulate.transitions) 55 | 56 | replyEvents ++ decision.events ++ simulate.events 57 | } 58 | 59 | def applyStateTransitionToLiving(living:Living, transitions:Seq[StateTransition[Living]]) : Seq[Event] = { 60 | Seq.empty 61 | } 62 | 63 | def applyStateTransitionToEntity(entity:Entity, transitions:Seq[StateTransition[Entity]]) : Seq[Event] = { 64 | Seq.empty 65 | } 66 | 67 | def updateItem(world:World, item:Item, id:Int) : Seq[Event] = { 68 | val replyEvents = item.events.flatMap { ev => 69 | applyItemEvent(item, ev, id) 70 | } 71 | 72 | val simulate = Simulate.simulate(world, item) 73 | 74 | applyStateTransitionToEntity(item, simulate.transitions) 75 | 76 | replyEvents ++ simulate.events 77 | } 78 | 79 | def applyItemEvent(item:Item, ev:Event, id:Int) : Seq[Event] = { 80 | ev match { 81 | case PickupItem(_, from) => { 82 | if(item.alive) { 83 | Seq(SuccessfulPickup(item.itemType, from)) 84 | } else { 85 | Seq(FailedPickup(id, from)) 86 | } 87 | } 88 | } 89 | } 90 | 91 | def applyLivingEvent(living:Living, ev:Event, id:Int) : Seq[Event] = { 92 | ev match { 93 | case PlaceItemSucceeded(itemType, _) => 94 | case PlaceItemFailed(itemType, _) => living.itemInHand = Some(itemType) 95 | case SuccessfulPickup(itemType, _) => living.itemInHand = Some(itemType) 96 | case FailedPickup(entityId, _) => 97 | case TakeDamage(damage, from, _) => { 98 | living.health -= damage 99 | living.alive = living.health > 0 100 | } 101 | } 102 | Seq.empty 103 | } 104 | } -------------------------------------------------------------------------------- /src/main/scala/game/event/Event.scala: -------------------------------------------------------------------------------- 1 | package game.event 2 | 3 | import game.state.{Vec2i, Living, Entity} 4 | 5 | trait Event 6 | 7 | trait FromEntityEvent extends Event { 8 | def from:Int 9 | } // all events are routed 10 | trait ToEntityEvent extends Event { def to:Int } 11 | 12 | // doesn't cause a state transition, just a flag to hold on to it rather than mutate 13 | // mainly used for failures, or any event that doesn't transition state 14 | 15 | trait ZoneEvent extends Event // routed to event 16 | 17 | case class DecisionOutput(events:Seq[Event], transitions: Seq[InstantStateTransition[Living]]) 18 | case class SimulationOutput(events:Seq[Event], transitions: Seq[StateTransition[Entity]]) 19 | 20 | 21 | // ROUTED EVENTS 22 | case class PlaceItemAt(at:Vec2i, itemType:Int, from:Int) extends FromEntityEvent with ZoneEvent { 23 | def failure = PlaceItemFailed(itemType, from) 24 | } 25 | case class PlaceItemFailed(itemType:Int, to:Int) extends ToEntityEvent 26 | case class PlaceItemSucceeded(itemType:Int, to:Int) extends ToEntityEvent 27 | 28 | case class RequestPath(fromPosition:Vec2i, toPosition:Vec2i, from:Int) extends FromEntityEvent { 29 | def failure = PathRequestFailed(from) 30 | } 31 | 32 | case class PathRequestSuccessful(path:Seq[Int], to:Int) extends ToEntityEvent 33 | case class PathRequestFailed(to:Int) extends ToEntityEvent 34 | 35 | case class PickupItem(to:Int, from:Int) extends FromEntityEvent with ToEntityEvent { 36 | def failure = FailedPickup(to, from) 37 | } 38 | case class SuccessfulPickup(itemType:Int, to:Int) extends ToEntityEvent 39 | case class FailedPickup(from:Int, to:Int) extends ToEntityEvent 40 | 41 | case class DropItem(itemType:Int, from:Int) extends FromEntityEvent with ZoneEvent // unsure if this is needed 42 | 43 | case class TakeDamage(damage:Int, from:Int, to:Int) extends FromEntityEvent with ToEntityEvent 44 | 45 | // STATE TRANSITIONS 46 | trait StateTransition[-T] { 47 | def applyTo(entity: T) 48 | } 49 | 50 | trait InstantStateTransition[T] extends StateTransition[T] // decide can only return instant state transitions 51 | case object DrawWeapon extends InstantStateTransition[Living] { 52 | def applyTo(living:Living) { 53 | living.weaponDrawn = true 54 | } 55 | } 56 | case object PutAwayWeapon extends InstantStateTransition[Living] { 57 | def applyTo(living:Living) = { 58 | living.weaponDrawn = false 59 | } 60 | } 61 | case object BeginAttack extends InstantStateTransition[Living] { 62 | def applyTo(living:Living) = { 63 | living.attacking = true 64 | } 65 | } 66 | case class ChangeFacing(facing:Vec2i) extends InstantStateTransition[Living] { 67 | def applyTo(living:Living) = { 68 | living.facing := facing 69 | } 70 | } 71 | case object Death extends InstantStateTransition[Entity] { 72 | def applyTo(entity:Entity) { 73 | entity.alive = false 74 | } 75 | } 76 | 77 | trait SimulatedStateTransition[T] extends StateTransition[T] // only update is allowed to return simulated state transitions (it can also return instants) 78 | case class ChangePosition(position:Vec2i) extends SimulatedStateTransition[Entity] { 79 | def applyTo(entity:Entity) { 80 | entity.position := position 81 | } 82 | } 83 | --------------------------------------------------------------------------------