Working towards the beginnings of serious settlement code.
This commit is contained in:
parent
29ed2dd0db
commit
0b34d81002
57
doc/Dynamic-consequences.md
Normal file
57
doc/Dynamic-consequences.md
Normal file
|
@ -0,0 +1,57 @@
|
|||
# On the consequences of a dynamic game environment for storytelling
|
||||
|
||||
First, a framing disclaimer: in [Racundra's First Cruise](https://books.google.co.uk/books?id=Ol1-DwAAQBAJ&lpg=PP1&pg=PT77#v=twopage&q&f=false), Arthur Ransome describes coming across a half built - and by the time he saw it, already obsolete - wooden sailing ship, in a Baltic forest. An old man was building it, by himself. He had been building it since he had been a young man. It's clear that Ransome believed the ship would never be finished. It's not clear whether the old man believed that it would, but nevertheless he was building it.
|
||||
|
||||
I will never build a complete version of The Great Game; it will probably never even be a playable prototype. It is a minor side-project of someone who
|
||||
|
||||
1. Is ill, and consequently has inconsistent levels of energy and concentration;
|
||||
2. Has other things to do in the real world which necessarily take precedence.
|
||||
|
||||
Nevertheless, in making design choices I want to specify something which could be built, which could, except for the technical innovations I'm trying myself to build, be built with the existing state of the art, and which if built, would be engaging and interesting to play.
|
||||
|
||||
The defining characteristic of Role Playing Games - the subcategory of games in which I am interested - is that the actions, decisions and choices of the player make a significant difference to the outcome of the plot, significantly affect change in the world. This already raises challenges for the cinematic elements in telling the game story, and those cinematic elements are one of the key rewards to the player, one of the elements of the game's presentation which most build, and hold, player engagement. These challenges are clearly expressed in two very good videos I've watched recently: [Who's Commanding Shepard in Mass Effect?](https://youtu.be/bm0S4cn_rfw), which discusses how much control the player actually has/should have over the decisions of the character they play as; and [What Happened with Mass Effect Andromeda’s Animation?](https://youtu.be/NmLPpcVQFJM), which discusses how the more control the player has, the bigger the task of authoring animation of all conversations and plot events becomes.
|
||||
|
||||
There are two key innovations I want to make in The Great Game which set it apart from existing Role Playing Games, both of which make the production of engaging cinematic presentation of conversation more difficult, nd I'll handle each in turn. But before I do, there's something I need to make clear about the nature of video games themselves: what they are for. Video games are a vehicle to tell stories, to convey narrative. They're a rich vehicle, because the narrative is not fixed: it is at least to some degree mutable, responsive to the input of the audience: the player.
|
||||
|
||||
Clear? Let's move on.
|
||||
|
||||
The innovations I am interested in are
|
||||
|
||||
## Unconstrained natural speech input/output
|
||||
|
||||
I want the player to be able to interact with non-player characters (and, indeed, potentially with other player characters, in a multi-player context) simply by speaking to them. This means that the things the player character says cannot be scripted: there is no way for the game designer to predict the full repertoire of the player's input. It also means that the game must construct, and put into the mouth of the non-player character being addressed, an appropriate response, given
|
||||
|
||||
1. The speech interpretation engine's interpretation of what it is the player said;
|
||||
2. The immediate game and plot context;
|
||||
3. The particular non-player character addressed's knowledge of the game world;
|
||||
4. The particular non-player character's attitude towards the player;
|
||||
5. The particular non-player character's speech idiosyncracies, dialect, and voice
|
||||
|
||||
and it must be pretty clear that the full range of potential responses is extremely large. Consequently, it's impossible that all non-player character speech acts can be voice acted; rather, this sort of generated speech must be synthesised. But a consequence of this is that the non-player character's facial animation during the conversation also cannot be motion captured from a human actor; rather, it, too, must be synthesized.
|
||||
|
||||
This doesn't mean that speech acts by non-player characters which make plot points or advance the narrative can't be voice acted, but it does mean that the voice acting must be consistent with the simulated voice used for that non-player character - which is to say, probably, that the non-player character must use a synthetic voice derived from the voice of that particular voice actor.
|
||||
|
||||
## Dynamic game environment
|
||||
|
||||
Modern Role Playing Games are, in effect, extremely complex state machines: if you do the same things in the same sequence, the same outcomes will always occur. In a world full of monsters, bandits, warring armies and other dangers, the same quest givers will be in the same places at the same times. They are clockwork worlds, filled with clockwork automata. Of course, this has the advantage that is makes testing easier - and in a game with a complex branching narrative and many quests, testing is inevitably hard.
|
||||
|
||||
My vision for The Great Game is different. It is that the economy - and with it, the day to day choices of non-player characters - should be modelled. This means, non-player characters may unexpectedly die. Of course, you could implement a tag for plot-relevant characters which prevents them being killed (except when required by the plot).
|
||||
|
||||
## Plot follows player
|
||||
|
||||
As Role Playing Games have moved towards open worlds - where the player's movement in the environment is relatively unconstrained - the clockwork has become strained. The player has to get to particular locations where particular events happen, and so the player has to be very heavily signposted. Another solution - which I'd like to explore - is 'plot follows character'. The player is free to wander at will in the world, and plot relevant events will happen on their path. And by that I don't mean that we associate a set of non-player characters which each quest - as current Role Playing Games do - and then uproot the whole set from wherever they normally live in the world and dumping down in the player's path; but rather, for each role in a quest or plot event, we define a set of characteristics required to fulfill that role, and then, when the player comes to a place where there are a set of characters who have those characteristics, the quest or plot event will happen.
|
||||
|
||||
## Cut scenes, cinematics and rewarding the player
|
||||
|
||||
There's no doubt at all that 'cut scenes' - in effect, short movies spliced into game play during which the player has no decisions to make but can simply watch the scene unroll - are elements of modern games which players enjoy, and see to some extent as 'rewards'. And in many games, these are beautifully constructed works. It is a very widely held view that the quality of cutscenes depends to a large degree on human authorship. The three choices I've made above:
|
||||
|
||||
1. We can't always know exactly what non-player characters will say (although perhaps we can in the context of cut scenes where the player has no input);
|
||||
2. We can't always know exactly which non-player characters will speak the lines;
|
||||
3. We can't predict what a non-player character will say in response to a question, or how long that will take;
|
||||
4. We can't always know where any particular plot event will take place.
|
||||
|
||||
Each of these, obviously, make the task of authoring an animation harder. The general summary of what I'm saying here is that, although in animating a conversation or cutscene what the animator is essentially animating is the skeletons of the characters, and, provided that all character models are rigged on essentially similar skeletons, substituting one character model for another in an animated scene isn't a huge issue, with so much unknowable it is impossible that hand-authoring will be practicable, and so a lot will depend on the quality of the conversation system not merely to to produce convincingly enunciated and emoted sound, but also appropriate character animation and attractive cinematography. As you will have learned from the Mass Effect analysis videos I linked to above, that's a big ask.
|
||||
|
||||
Essentially the gamble here is that players will find the much richer conversations, and consequent emergent gameplay, possible with non-player charcaters who have dynamic knowledge about their world sufficiently engaging to compensate for a less compelling cinematic experience. I believe that they would; but really the only way to find out would be to try.
|
||||
|
||||
Interestingly, an [early preview](https://youtu.be/VwwZx5t5MIc?t=327) of CD PRoject Red's not-yet-complete [Cyberpunk 2077]() suggests that there will be very, very few cutscenes, suggesting that these very experienced storytellers don't feel they need cutscenes either to tell their story or maintain player engagement. (Later) It has to be said other commentators who have also played the Cyberpunk 2077 preview say that there are **a lot** of cutscenes, one of them describing the prologue as 'about half cutscenes' - so this impression I formed may be wrong).
|
9
doc/building_on_microworld.md
Normal file
9
doc/building_on_microworld.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
# Building on Microworld
|
||||
|
||||
In [Settling a Game World](Settling-a-game-world.html) I intended that a world should be populated by setting agents - settlers - to explore the map and select places to settle according to particular rules. In the meantime, I've built [MicroWorld](https://github.com/simon-brooke/mw-ui), a rule driven cellular automaton which makes a reasonably good job of modelling human settlement. It works, and I now plan to use it, as detailed in this note; but there are issues.
|
||||
|
||||
First and formost, it's slow, and both processor and memory hungry. That means that at continent scale, a cell of one kilometre square is the minimum size which is really possible, which isn't small enough to create a settlement map of the density that a game will need. Even with 1km cells, even on the most powerful machines I have access to, a continent-size map will take many days to run.
|
||||
|
||||
Of course it would be possible to do a run at one km scale top identify areas which would support settlement, and then to do a run on a ten metre grid on each of those areas to more precisely plot settlement. That's an idea which I haven't yet explored, which might prove fruitful.
|
||||
|
||||
Secondly, being a cellular automaton, MicroWorld works on a grid. This means that everything is grid aligned, which is absolutely not what I want! So I think the way to leverage this is to use Microworld to establish which kilometre square cells om the grid should be populated (and roughly with what), and then switch to ad hoc code to populate those cells.
|
11
project.clj
11
project.clj
|
@ -5,10 +5,15 @@
|
|||
:output-path "docs/codox"
|
||||
:source-uri "https://github.com/simon-brooke/the-great-game/blob/master/{filepath}#L{line}"}
|
||||
:cucumber-feature-paths ["test/features/"]
|
||||
:dependencies [[org.clojure/clojure "1.8.0"]
|
||||
[org.clojure/math.numeric-tower "0.0.4"]
|
||||
:dependencies [[com.taoensso/timbre "4.10.0"]
|
||||
[environ "1.1.0"]
|
||||
[com.taoensso/timbre "4.10.0"]]
|
||||
[journeyman-cc/walkmap "0.1.0-SNAPSHOT"]
|
||||
[me.raynes/fs "1.4.6"]
|
||||
[mw-engine "0.1.6-SNAPSHOT"]
|
||||
[org.clojure/algo.generic "0.1.3"]
|
||||
[org.clojure/clojure "1.8.0"]
|
||||
[org.clojure/math.numeric-tower "0.0.4"]
|
||||
]
|
||||
:description "Prototype code towards the great game I've been writing about for ten years, and know I will never finish."
|
||||
:license {:name "GNU General Public License,version 2.0 or (at your option) any later version"
|
||||
:url "https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html"}
|
||||
|
|
1
resources/maps/barra/barra.edn
Normal file
1
resources/maps/barra/barra.edn
Normal file
|
@ -0,0 +1 @@
|
|||
[]
|
1
resources/maps/barra/barra.html
Normal file
1
resources/maps/barra/barra.html
Normal file
|
@ -0,0 +1 @@
|
|||
<html><head><title>Microworld render</title><link href="https://www.journeyman.cc/mw-ui-assets/css/states.css" rel="stylesheet" type="text/css" /></head><body><table></table></body></html>
|
BIN
resources/maps/barra/barra.png
Normal file
BIN
resources/maps/barra/barra.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.1 KiB |
BIN
resources/maps/barra/barra.xcf
Normal file
BIN
resources/maps/barra/barra.xcf
Normal file
Binary file not shown.
1
resources/maps/barra/barra_100.edn
Normal file
1
resources/maps/barra/barra_100.edn
Normal file
File diff suppressed because one or more lines are too long
1
resources/maps/barra/barra_100.html
Normal file
1
resources/maps/barra/barra_100.html
Normal file
File diff suppressed because one or more lines are too long
2256
resources/maps/barra/barra_100_edited.edn
Normal file
2256
resources/maps/barra/barra_100_edited.edn
Normal file
File diff suppressed because it is too large
Load diff
1
resources/maps/barra/barra_101.edn
Normal file
1
resources/maps/barra/barra_101.edn
Normal file
File diff suppressed because one or more lines are too long
1
resources/maps/barra/barra_101.html
Normal file
1
resources/maps/barra/barra_101.html
Normal file
File diff suppressed because one or more lines are too long
BIN
resources/maps/noise.png
Normal file
BIN
resources/maps/noise.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
BIN
resources/maps/noise.xcf
Normal file
BIN
resources/maps/noise.xcf
Normal file
Binary file not shown.
|
@ -33,3 +33,13 @@
|
|||
(list (first %) 'm)
|
||||
(nth % 1))
|
||||
targets)))))
|
||||
|
||||
(defn value-or-default
|
||||
"Return the value of this key `k` in this map `m`, or this `dflt` value if
|
||||
there is none."
|
||||
[m k dflt]
|
||||
(or (when (map? m) (m k)) dflt))
|
||||
|
||||
;; (value-or-default {:x 0 :y 0 :altitude 7} :altitude 8)
|
||||
;; (value-or-default {:x 0 :y 0 :altitude 7} :alt 8)
|
||||
;; (value-or-default nil :altitude 8)
|
||||
|
|
158
src/the_great_game/world/heightmap.clj
Normal file
158
src/the_great_game/world/heightmap.clj
Normal file
|
@ -0,0 +1,158 @@
|
|||
(ns the-great-game.world.heightmap
|
||||
"Functions dealing with the tessellated multi-layer heightmap."
|
||||
(:require [clojure.math.numeric-tower :refer [expt sqrt]]
|
||||
[mw-engine.core :refer []]
|
||||
[mw-engine.heightmap :refer [apply-heightmap]]
|
||||
[mw-engine.utils :refer [get-cell in-bounds? map-world]]
|
||||
[the-great-game.utils :refer [value-or-default]]))
|
||||
|
||||
;; It's not at all clear to me yet what the workflow for getting a MicroWorld
|
||||
;; map into The Great Game, and whether it passes through Walkmap to get here.
|
||||
;; This file as currently written assumes it doesn't.
|
||||
|
||||
;; It's utterly impossible to hold a whole continent at one metre scale in
|
||||
;; memory at one time. So we have to be able to regenerate high resolution
|
||||
;; surfaces from much lower resolution heightmaps.
|
||||
;;
|
||||
;; Thus to reproduce a segment of surface at a particular level of detail,
|
||||
;; we:
|
||||
;; 1. load the base heightmap into a grid (see
|
||||
;; `mw-engine.heightmap/apply-heightmap`);
|
||||
;; 2. scale the base hightmap to kilometre scale (see `scale-grid`);
|
||||
;; 3. exerpt the portion of that that we want to reproduce (see `exerpt-grid`);
|
||||
;; 4. interpolate that grid to get the resolution we require (see
|
||||
;; `interpolate-grid`);
|
||||
;; 5. create an appropriate purturbation grid from the noise map(s) for the
|
||||
;; same coordinates to break up the smooth interpolation;
|
||||
;; 6. sum the altitudes of the two grids.
|
||||
;;
|
||||
;; In production this will have to be done **very** fast!
|
||||
|
||||
(def ^:dynamic *base-map* "resources/maps/heightmap.png")
|
||||
(def ^:dynamic *noise-map* "resources/maps/noise.png")
|
||||
|
||||
(defn scale-grid
|
||||
"multiply all `:x` and `:y` values in this `grid` by this `n`."
|
||||
[grid n]
|
||||
(map-world grid (fn [w c x] (assoc c :x (* (:x c) n) :y (* (:y c) n)))))
|
||||
|
||||
|
||||
|
||||
;; Each of the east-west curve and the north-south curve are of course two
|
||||
;; dimensional curves; the east-west curve is in the :x/:z plane and the
|
||||
;; north-south curve is in the :y/:z plane (except, perhaps unwisely,
|
||||
;; we've been using :altitude to label the :z plane). We have a library
|
||||
;; function `walkmap.edge/intersection2d`, but as currently written it
|
||||
;; can only find intersections in :x/:y plane.
|
||||
;;
|
||||
;; TODO: rewrite the function so that it can use arbitrary coordinates.
|
||||
;; AFTER TRYING: OK, there are too many assumptions about the way that
|
||||
;; function is written to allow for easy rotation. TODO: think!
|
||||
|
||||
(defn interpolate-altitude
|
||||
"Return the altitude of the point at `x-offset`, `y-offset` within this
|
||||
`cell` having this `src-width`, taken from this `grid`."
|
||||
[cell grid src-width x-offset y-offset ]
|
||||
(let [c-alt (:altitude cell)
|
||||
n-alt (or (:altitude (get-cell grid (:x cell) (dec (:y cell)))) c-alt)
|
||||
w-alt (or (:altitude (get-cell grid (inc (:x cell)) (:y cell))) c-alt)
|
||||
s-alt (or (:altitude (get-cell grid (:x cell) (inc (:y cell)))) c-alt)
|
||||
e-alt (or (:altitude (get-cell grid (dec (:x cell)) (:y cell))) c-alt)]
|
||||
;; TODO: construct two curves (arcs of circles good enough for now)
|
||||
;; n-alt...c-alt...s-alt and e-alt...c-alt...w-alt;
|
||||
;; then interpolate x-offset along e-alt...c-alt...w-alt and y-offset
|
||||
;; along n-alt...c-alt...s-alt;
|
||||
;; then return the average of the two
|
||||
|
||||
0))
|
||||
|
||||
(defn interpolate-cell
|
||||
"Construct a grid (array of arrays) of cells each of width `target-width`
|
||||
from this `cell`, of width `src-width`, taken from this `grid`"
|
||||
[cell grid src-width target-width]
|
||||
(let [offsets (map #(* target-width %) (range (/ src-width target-width)))]
|
||||
(into
|
||||
[]
|
||||
(map
|
||||
(fn [r]
|
||||
(into
|
||||
[]
|
||||
(map
|
||||
(fn [c]
|
||||
(assoc cell
|
||||
:x (+ (:x cell) c)
|
||||
:y (+ (:y cell) r)
|
||||
:altitude (interpolate-altitude cell grid src-width c r)))
|
||||
offsets)))
|
||||
offsets))))
|
||||
|
||||
(defn interpolate-grid
|
||||
"Return a grid interpolated from this `grid` of rows, cols given scaling
|
||||
from this `src-width` to this `target-width`"
|
||||
[grid src-width target-width]
|
||||
(reduce
|
||||
concat
|
||||
(into
|
||||
[]
|
||||
(map
|
||||
(fn [row]
|
||||
(reduce
|
||||
(fn [g1 g2]
|
||||
(into [] (map #(into [] (concat %1 %2)) g1 g2)))
|
||||
(into [] (map #(interpolate-cell % grid src-width target-width) row))))
|
||||
grid))))
|
||||
|
||||
(defn excerpt-grid
|
||||
"Return that section of this `grid` where the `:x` co-ordinate of each cell
|
||||
is greater than or equal to this `x-offset`, the `:y` co-ordinate is greater
|
||||
than or equal to this `y-offset`, whose width is not greater than this
|
||||
`width`, and whose height is not greater than this `height`."
|
||||
[grid x-offset y-offset width height]
|
||||
(into
|
||||
[]
|
||||
(remove
|
||||
nil?
|
||||
(map
|
||||
(fn [row]
|
||||
(when
|
||||
(and
|
||||
(>= (:y (first row)) y-offset)
|
||||
(< (:y (first row)) (+ y-offset height)))
|
||||
(into
|
||||
[]
|
||||
(remove
|
||||
nil?
|
||||
(map
|
||||
(fn [cell]
|
||||
(when
|
||||
(and
|
||||
(>= (:x cell) x-offset)
|
||||
(< (:x cell) (+ x-offset width)))
|
||||
cell))
|
||||
row)))))))))
|
||||
|
||||
(defn get-surface
|
||||
"Return, as a vector of vectors of cells represented as Clojure maps, a
|
||||
segment of surface from this `base-map` as modified by this
|
||||
`noise-map` at this `cell-size` starting at this `x-offset` and `y-offset`
|
||||
and having this `width` and `height`.
|
||||
|
||||
If `base-map` and `noise-map` are not supplied, the bindings of `*base-map*`
|
||||
and `*noise-map*` will be used, respectively.
|
||||
|
||||
`base-map` and `noise-map` may be passed either as strings, assumed to be
|
||||
file paths of PNG files, or as MicroWorld style world arrays. It is assumed
|
||||
that one pixel in `base-map` represents one square kilometre in the game
|
||||
world. It is assumed that `cell-size`, `x-offset`, `y-offset`, `width` and
|
||||
`height` are integer numbers of metres."
|
||||
([cell-size x-offset y-offset width height]
|
||||
(get-surface *base-map* *noise-map* cell-size x-offset y-offset width height))
|
||||
([base-map noise-map cell-size x-offset y-offset width height]
|
||||
(let [b (if (seq? base-map) base-map (scale-world (apply-heightmap base-map) 1000))
|
||||
n (if (seq? noise-map) noise-map (apply-heightmap noise-map))]
|
||||
(if (and (in-bounds? b x-offset y-offset)
|
||||
(in-bounds? b (+ x-offset width) (+ y-offset height)))
|
||||
b ;; actually do stuff
|
||||
(throw (Exception. "Surface out of bounds for map.")))
|
||||
)))
|
||||
|
7
src/the_great_game/world/mw.clj
Normal file
7
src/the_great_game/world/mw.clj
Normal file
|
@ -0,0 +1,7 @@
|
|||
(ns the-great-game.world.mw
|
||||
"Functions dealing with building a great game world from a MicroWorld world."
|
||||
(:require [clojure.math.numeric-tower :refer [expt sqrt]]
|
||||
[mw-engine.core :refer []]
|
||||
[mw-engine.world :refer []]))
|
||||
|
||||
;; It's not at all clear to me yet what the workflow for getting a MicroWorld map into The Great Game, and whether it passes through Walkmap to get here. This file as currently written assumes it doesn't.
|
Loading…
Reference in a new issue