SimCity is also known as Micropolis, a city-building simulation video game developed by Will Wright. In January 2008, the SimCity source code was released under the free software GPL 3 license, renamed to Micropolis (the original working title) for trademark reasons.
ππ»This is how Micropolis looked like. If you played it before, it is all coming back to you, right?
The Sims Series has been one of my all time favorites.
As the original SimCity (1989) didn't have any type of arts or education in the systems, I decided to launch this two-week project to add a new type of building -- Museum into the game. The source code I used is the translated Micropolis source code in Java, forked from David Culyba , who was my instructor for the course Programming for Game Designers at Carnegie Mellon University.
Let's "artify" the beloved city!
My primary design documentation is a one-page design chart that concisely concludes the implementation plan. This design approach is inspired by Stone Librande's GDC talk on one-page design.
In the one-page fashion, readers will be able to understand that particular core mechanic in the most intuitive way. As each mechanic is designed within an one-pager, a sophisticated game consists of multiple stacks of one-pagers.
The complete PDF version of the one-page design is here: Design Chart
ππ»Here is a snippet of the main design document:
It can be easily seen from the design doc that adding a museum will involve three main aspects:
- Land value will increase
- Crime rate will decrease
- Education coverage will increase (also, there will be an education coverage map to let players know how impactful the museums are, and what regions are impacted)
The reasons why these three parts should be implemented will be expanded on in the section below.
Having museums in the city means embracing education and arts. Beyond those, thinking of golden-standard museums such as the Musuem of Modern Art at NYC, good museums also bring about climbing land values, flow-in migration, subtle decrease of crime, and other socio-economic impact. To reflect this kind of change in the game, the system designer must be clearly aware of the internal relationships between different parameters, because the parameters out there are not all independent -- some affect others.
For example, climbing land values might stimulate crime while the education impact museums bring might mitigate crime. On the other hand, climbing land values are supposed to push people away to other cities; however, like NYC, some people are willing to live close to MoMA despite the housing price.
Thus, as Micropolis already has an established internally connected system as a whole, the addition of museum should only change the most directly related parameters, as these changed parameters will continue to affect other related ones. Land value, crime, and nearby education coverage are the three main directly affected metrics of the city. This is why I have chosen the implementation plan as shown in my design doc.
Adding one extra mechanic is not like simply squeezing some equations into the whole program. Instead, it is to introduce a whole new feature and to have it properly interact with the existing convoluted mechanics/systems.
Now, education coverage has been successfully embedded into the map overlays. If the player clicks the map, it will show the coverage of the museums. Also, as museums also affect population, land price, crime, and many other factors, players will be able to see how other parameters are affected in their respective maps.
ππ» Here is how the education map is embedded in the existing map system:
ππ» This is how the education coverage map looks like:
βπ» The effect of the spread-out pixels on the maps are created using a smooth-out function which has a kernel that moves upon the map tile grids to make arithmetic changes so that the pixels are smoothly smudged.
To successfully add a museum mechanic into the SimCity, there are several important steps.
I decided that the museum should be the same size as the police station as well as the fire station (3 * 3 size). And I later used Apple Preview to paint the icon (I would like to apologize for the incorrect shading of the building -- I was not familiar with any existing pixel drawing tool. Please bear with my limited toolset).
ππ»This is how the icons look like:
As Micropolis is 2D tile-based game, every different building type has a corresponding hardcoded index to the a particular sprite. These indexes are stored in a public class as tile constants.
For example, dirt patches are indexed as 0 and 1; river sprites start from 3, etc. ππ»This is the index "dictionary" containing 936 tiles in total, provided by Chaim Gingold:
ππ» This is the public class that stores tile constants: It matches the spritesheet provided above. (Some indexes are slightly different from Gingold's chart due to this ported Java version.)
public class TileConstants{
public static final short CLEAR = -1;
public static final char DIRT = 0;
static final char RIVER = 2;
static final char REDGE = 3;
static final char CHANNEL = 4;
static final char RIVEDGE = 5;
static final char FIRSTRIVEDGE = 5;
static final char LASTRIVEDGE = 20;
static final char TREEBASE = 21;
static final char WOODS_LOW = TREEBASE;
static final char WOODS = 37;
static final char WOODS_HIGH = 39;
static final char WOODS2 = 40;
static final char WOODS5 = 43;
static final char RUBBLE = 44;
static final char LASTRUBBLE = 47;
// the index goes to 936...
}
Thus, to successfully register a museum into the system, I had to create a new index for the museum so the system will identity the building type when scanning all the tiles.ππ»
static final char MUSEUM = 964;
The museum sprite will be sliced to 9 pieces (3* 3), as one piece takes up one tile.
ππ»Code Snippet:
# BEGIN MUSEUM #
960 museum@0,0 (conducts)
961 museum@16,0 (conducts)
962 museum@32,0 (conducts)
963 museum@0,16 (conducts)
964 museum@16,16 (zone)(conducts)(building=3x3)(behavior=MUSEUM)(description=#28)
965 museum@32,16 (conducts)
966 museum@0,32 (conducts)
967 museum@16,32 (conducts)
968 museum@32,32 (conducts)
Other than static index, adding museums requires following implementations:
ππ»Code Snippet:
public enum MicropolisTool{
BULLDOZER(1, 1),
WIRE(1, 5), //cost=25 for underwater
ROADS(1, 10), //cost=50 for over water
RAIL(1, 20), //cost=100 for underwater
RESIDENTIAL(3, 100),
COMMERCIAL(3, 100),
INDUSTRIAL(3, 100),
FIRE(3, 500),
POLICE(3, 500),
STADIUM(4, 5000),
PARK(1, 10),
SEAPORT(4, 3000),
POWERPLANT(4, 3000),
NUCLEAR(4, 5000),
AIRPORT(6, 10000),
MUSEUM(3, 1000),
QUERY(1, 0);
}
ππ»Code Snippet:
boolean apply1(ToolEffectIfc eff){
switch (tool){
case FIRE: return applyZone(eff, FIRESTATION);
case POLICE: return applyZone(eff, POLICESTATION);
case POWERPLANT: return applyZone(eff, POWERPLANT);
case STADIUM: return applyZone(eff, STADIUM);
case SEAPORT: return applyZone(eff, PORT);
case NUCLEAR: return applyZone(eff, NUCLEAR);
case AIRPORT: return applyZone(eff, AIRPORT);
case MUSEUM: return applyZone(eff, MUSEUM);
default:throw new Error("unexpected tool: "+tool);
}
}
ππ»Code Snippet: (many other non-museum behaviors are removed for simplicity)
Map<String,TileBehavior> tileBehaviors;
void initTileBehaviors(){
HashMap<String,TileBehavior> bb;
bb = new HashMap<String,TileBehavior>();
bb.put("FIRE", new TerrainBehavior(this, TerrainBehavior.B.FIRE));
bb.put("FLOOD", new TerrainBehavior(this, TerrainBehavior.B.FLOOD));
bb.put("RADIOACTIVE", new TerrainBehavior(this, TerrainBehavior.B.RADIOACTIVE));
bb.put("ROAD", new TerrainBehavior(this, TerrainBehavior.B.ROAD));
bb.put("RAIL", new TerrainBehavior(this, TerrainBehavior.B.RAIL));
bb.put("EXPLOSION", new TerrainBehavior(this, TerrainBehavior.B.EXPLOSION));
bb.put("RESIDENTIAL", new MapScanner(this, MapScanner.B.RESIDENTIAL));
bb.put("FIRESTATION", new MapScanner(this, MapScanner.B.FIRESTATION));
bb.put("MUSEUM", new MapScanner(this, MapScanner.B.MUSEUM));
this.tileBehaviors = bb;
}
ππ»Code Snippet:
void doMuseum(){
boolean powerOn = checkZonePower();
city.museumCount++;
if ((city.cityTime % 8) == 0) {
repairZone(MUSEUM, 3);
}
if (powerOn) {
museumPopGrowth();
}
}
void museumPopGrowth() {
double rpop = city.resPop * 1.5;
city.resPop = (int)Math.ceil(rpop);
double cpop = city.comPop * 1.2;
city.comPop = (int)Math.ceil(cpop);
double ipop = city.indPop * 1.1;
city.indPop = (int)Math.ceil(ipop);
}
ππ»Code Snippet: (many other non-museum behaviors are removed for simplicity)
public void apply(){
switch (behavior) {
case RESIDENTIAL:
doResidential();
return;
case HOSPITAL_CHURCH:
doHospitalChurch();
return;
case COMMERCIAL:
doCommercial();
return;
case INDUSTRIAL:
doIndustrial();
return;
case MUSEUM:
doMuseum();
return;
default: assert false;
}
}
ππ»Code Snippet:
public class ZoneStatus{
/** Number from 0 to 27, identifying the type of building. */
public int building;
/** Number from 1 to 4, 1=Low, 2=Medium, 3=High, 4=Very High. */
public int popDensity;
/** Number from 5 to 8, 5=Slum, 6=Lower Class, etc. */
public int landValue;
/** Number from 9 to 12, 9=Safe, 10=Light, 11=Moderate, etc. */
public int educationCoverage;
public int crimeLevel;
/** Number from 13 to 16, 13=None, 14=Moderate, 15=Heavy, etc. */
public int pollution;
/** Number from 17 to 20, 17=Declining, 18=Stable, etc. */
public int growthRate;
}
ππ»Code Snippet:
public enum MapState{
ALL, //ALMAP
RESIDENTIAL, //REMAP
COMMERCIAL, //COMAP
INDUSTRIAL, //INMAP
TRANSPORT, //RDMAP
POPDEN_OVERLAY, //PDMAP
GROWTHRATE_OVERLAY, //RGMAP
LANDVALUE_OVERLAY, //LVMAP
MUSEUM_EDUCATION_OVERLAY,
CRIME_OVERLAY, //CRMAP
POLLUTE_OVERLAY, //PLMAP
TRAFFIC_OVERLAY, //TDMAP
POWER_OVERLAY, //PRMAP
FIRE_OVERLAY, //FIMAP
POLICE_OVERLAY; //POMAP
}
ππ»Code Snippet:
void checkMuseum() {
if (cityTime % 2 == 0) {
MicropolisMessage z = null;
if (museumCount - lastMuseumCount == 1) {
z = MicropolisMessage.NEW_MUSEUM;
}
if (z != null) {
sendMessage(z);
}
lastMuseumCount = museumCount;
}
}
ππ»Code Snippet:
public int getEducationImpact(int xpos, int ypos) {
if (testBounds(xpos, ypos)) {
return museumMap[ypos/2][xpos/2];
}
else {
return 0;
}
}
ππ»Code Snippet:
private void drawEducationMap(Graphics gr){
int [][] A = engine.museumMap;
for (int y = 0; y < A.length; y++) {
for (int x = 0; x < A[y].length; x++) {
maybeDrawRect(gr, getCI(10 + A[y][x]),x*6,y*6,6,6);
}
}
}
ππ»Code Snippet:
private int checkEducationOverlay(BufferedImage img, int xpos, int ypos, int tile){
int e = engine.getEducationImpact(xpos, ypos);
Color c = getCI(e);
if (c == null) {
return tile;
}
int pix = c.getRGB();
for (int yy = 0; yy < TILE_HEIGHT; yy++) {
for (int xx = 0; xx < TILE_WIDTH; xx++) {
img.setRGB(xpos*TILE_WIDTH+xx,ypos*TILE_HEIGHT+yy,pix);
}
}
return CLEAR;
}
π Update the message system to notify players of the building of a new museum and the demand for museums
ππ»Code Snippet: (many other non-museum messages are removed for simplicity)
public enum MicropolisMessage{
NEED_RES,
NEED_COM,
NEED_IND,
NEED_ROADS,
NEED_RAILS,
NEED_POWER,
POP_2K_REACHED,
POP_10K_REACHED,
POP_50K_REACHED,
POP_100K_REACHED,
POP_500K_REACHED,
NEW_MUSEUM,
NEED_MUSEUM;
}
I also would like to address the future scope here. SimCity is a very good practice for game designers to create new mechanics, despite the archaic convention of Java coding. I would like to add more interesting buildings in the future -- currently I am thinking of universities, elementary schools for the city education. Also, maybe adding a strip club could be interesting.
And I have to admit that Micropolis is a fun game for not only players but also game developers.