There is a lot that goes into the development of a game and Junction Gate is no exception. In this post, I'd like to take you behind-the-scenes to look at the development of the Trade Center in the upcoming Junction Gate beta. The Trade Center will be just one part of the first phase of trading that will appear in the next beta release. This is a technical post, so there will be a lot of code, but there will also be screenshots and a video at the end.
One additional note before diving in: There will be no update next week. The next update will be sometime during the week of April 18-22, but perhaps later in the week than the normal Tuesday.
If you recall the wireframing post on trading, the trading mechanics and screens were prototyped using Balsamiq Mockups. The Trade Center is a facility on Junction Gate that will allow the player to place bids to purchase or trade for goods on the market and to make sales proposals for selling their own goods. Ships will then visit Junction Gate and either buy or sell at those prices or initiate a trade for other goods.
Here is the wireframe for the Trade Center screen:
Everything seemed fine in the wireframe design, so I began to implement it in code, first starting with the menu and the headers.
So far, so good. But then I started coding the table for the purchases column.
Uh oh. It quickly became clear that the wireframe design wasn't going to work in the actual game. The table overflowed the column by quite a bit. The row heights in the table weren't uniform because some of the longer component names broke the layout. The second column wasn't going to fit.
There were several solutions available: a smaller font size, reducing the input widths, and truncating the component names with an ellipsis. However, each of those solutions reduced the readability of the information in the table.
Instead, I decided to change the columns to full-width tabs. While I had wanted the player to be able to see both the buying and selling screens at the same time, a tabbed design allows more room for the data to breathe, reduces visual clutter, lets the player focus on one task at a time, and also has the added bonus of fitting in with the tabbed design of some of the other trading wireframes.
Issues like this are common in development. As a developer or a designer, one needs to be flexible in approaching any project, even with a plan, because the act of implementation always surfaces some nuance or glaring issue for which the plan didn't account. This example, while minor, illustrates why most game developers preface any work they show their audience as being subject to change. Games are iterative projects and sometimes plans or features need to change because of unanticipated circumstances.
One takeaway in this case is that wireframing has a weakness: it's not pixel perfect. Most of the time the lack of pixel-perfection won't be an issue. At times, it can even be a strength. But in this instance, my wireframe screen was larger than the actual in-game screen, and so the issue didn't surface until implementation. It's a learning moment, to be sure, and I hope that in the future I will be able to prevent it from happening again. However, it seems likely that some of the other trading wireframes may require further iteration as well.
The codebase is divided into several sections. The directory of the project looks like this:
The styles of the game are stored in the themes directory. A theme is composed of CSS files, images, and fonts. Currently, there is only one theme, but once the game is complete, there may be opportunities to expand that number.
Now to the actual code. We'll go function by function, with explanations and progress screenshots throughout. I won't go into every detail about the framework, but I'll cover the basics and you should get a sense of how things work. At the end, I'll list the full files to make it easier to read all at once.
First, let's create the language file for the Trade Center screen, which will have the following location:
This is a simple JSON file of key/value pairs, one on each line. The key on the left of the colon is what is used in the template files to reference a string. The value, on the right side of the colon, is the translation. The key will stay the same in every language while the value will be translated to that particular language. A good key will be descriptive of its translation, but it does not necessarily need to be word-for-word.
There is some text you will see in the template that's not visible here. Those strings are stored in another language file that keeps track of more commonly used text.
Next is the template file,
templates/commercial/tradeCenter.js. All template files have a common function called
render, which is responsible for compiling everything that the screen should display and sending it back to the engine. The
render function will be called by the engine anytime a template is loaded and then injected onto the page.
I won't go into every line of code, but I will highlight some of the more interesting or obscure parts and reference them by line number.
_.f() creates a document fragment (the f is for fragment), which acts as a temporary container for the template HTML without actually creating an HTML container when the template is rendered. The underscore at the beginning is a namespace for the framework's custom utility library, much like Underscore.js or lodash.
_.e('div.header') creates a
div element (the e is for element) with a class of
header. The shorthand class assignment to the element works just like a CSS selector and makes it easy to assign one or more classes to an elmenent. Ids can also be assigned with a
#. You will see in later examples of this function that you can also specify individual attributes by passing in an object as a second parameter.
There are a number of UI helper functions in the framework, making it easy to reuse code and quickly create a complex UI element. In this section of code, there are two UI helpers,
jg.ui.list. The list helper has additional methods for adding list items, so it also requires a
render function. However, the breadcrumb helper is simpler and renders automatically.
Here we are retrieving the options for the trade good category dropdown select box. Because there are still component categories that need to be added to the game, it's easier to retrieve the entire component list and compile the categories dynamically rather than list them out individually in code. This way, if I change the names of the component categories or add more in the future, I won't need to update this part of the game; it will be done automatically. The options list is then sorted alphabetically to make using it easier.
Notice also the
_.translate() function on line 21. The string passed into that function is a language key, like the keys in the language file above.
The display is the semi-transparent blue section in the user interface.
This final section of the
render function creates the tabs and tab panel container elements. The content of the tab panels is generated elsewhere by calling the
renderSellTab functions on lines 49, 50, 62, and 63.
A future refactor of this code would probably turn the tabs setup code into a UI helper.
The rendering of the purchase tab panel is broken up into several functions. The first is
The dropdown select options that we compiled before are passed in as a parameter and it's here that the select box is actually rendered thanks to another UI helper function.
Lines 18-19 create a container for the purchase table and then call
renderPurchaseTable on a delay of 100ms. The table rendering is separated because we want to be able to update only that portion of the page when the user uses the category select box. Whenever
renderPurchaseTable is executed, it will erase the contents of
#tradeCenterPurchaseTable and replace it with the proper table. The delay is necessary because of the erase/replace behavior of
renderPurchaseTable as the template container first needs time to be rendered on the page.
renderPurchaseTable function is a longer chunk of code because building a table of data can only be so succinct. However, here we'll deal with the rendering only and leave the data retrieval to other functions.
Here we need to re-establish the scope of the function because it will sometimes be called from a user event, which changes the scope to the event object.
The goods are a combination of resources and components, as an array populated with an object for each good. In the future, other types of goods might also be included.
jg.ui.table() is another UI helper function.
table.setHeaders() generates the table headers and keeps track of how the table is sorted and by which column.
goods array of objects is iterated over to produce the table rows. There are 2 types of rows possible: an input row or a data row. The input rows allow the player to place a bid on a good while the data rows display goods that already have bids.
The first column of each row will display the name of the good. Clicking on the name will trigger a modal with more information on the good. These modals will be found throughout the game and it's important to put them in so that the user has easy access to the information.
Getting a list of goods is a straightforward process. Both the resources and components are stored separately, so they will both need to be retrieved and then normalized in the same format so they can be passed back to the table to be rendered.
This is how the resources and components data files are retrieved. Before creating the Trade Center, the resources data file didn't exist. Examples for both formats will be shown below.
items is used for the components retrieval. At one point, all components were called items and so this is a bit of technical debt that will need to eventually be resolved.
Some of the data needs to be calculated before being sent for rendering. Because each of these functions only deals with data, a new file,
This is the full data file for the resources. Most of it should be self-explanatory, but one interesting tidbit is that the mass for each resource is based on the amount of that resource that would fit into a cubic meter.
Because the list of components is much longer, I've only listed a single component to showcase the format. Components are more complex than resources, even though they share some common elements. Components can be composed of resources or other components and some are unlocked with research. Data files like this make it extremely easy to add new elements to the game. It also makes balancing changes a cinch.
Displaying the purchase table in the Trade Center requires additional calculations which are done in
When the player places a order for purchase or a proposal to sell, the bid is assigned to a variable in the holdings object. A holding is basically a colony, owned by the player or another computer faction.
Here we simply average out all of the bids for a particular good. However, because there are no factions in the game yet, we need to establish a base price for the good to create an average, which is done in line 9 by calling
this.getGoodBasePrice(). In the future, the base price will also act as an anchor for the market.
Because components are more complex than resources and may be composed of other components, one call to the
getGoodBasePrice function might not be enough. For this reason,
getGoodBasePrice has the ability to call itself again (see line 28). This is called a recursive function and it will execute as many times as it needs to until it has calculated the full cost of a component.
In all of these functions, we're performing basic checks to see if a holding has a bid for a good so that the table renderer knows what type of row it should render.
Now that the table is rendering properly, it's time to create the ability for the player to place bids from the table. This will require UI interactions, form validation, and some more calculations. First, let's address the UI interactions and the form validation, which are done in the
When the player clicks the Bid button, the
placeBid function will be called. In this section of code, we're getting and formatting all of the data we need to make sure we can place the bid.
If an error occurs, we don't place the bid and instead add an error class to the input. Otherwise, if the error has been fixed, we remove it. This is the first part of validation. You can see an example of this in the screenshot above.
We attempt to place a bid by calling
jg.calculate.trade.placeTradeOrder, which is shown a little further down. Again, the order is proccessed in the engine, not the template, because it deals with data, not the UI. The added advantage to this is that it also can be used by computer factions as a part of the background simulation.
If the bid was successful, two parts of the page are re-rendered: the purchase table and the resource panel at the top of the screen. The resource panel is re-rendered because a purchase order will remove credits from the holding's accounts. While this normally refreshes on its own, it does not refresh when the game is paused, so it needs to be triggered manually.
If the bid was unsuccessful because the player did not have enough credits, we trigger an error modal, shown above. The error modal was chosen over an inline error message because it requires a longer explanation to fix and there isn't a clean way to include that explanation inline.
placeTradeOrder simply checks if the player can afford to place a bid and then executes the bid if it can, attaching the bid to the holding.
There are two types of bids that can be placed: a purchase order or a trade bid. The purchase order is paid for in credits, which are immediately removed from the player's account. If the order is cancelled before it is fulfilled, those credits are returned. The trade bid does not set aside credits because it indicates that the player would like to trade other with the ship once it arrives.
Cancelling bids is much simpler than placing them because no validation is necessary. A call to the
jg.calculate.trade.cancelTradeOrder function and then rerendering the table and the resource panel is all that a cancellation entails.
trade.js, the code is equally simple. First check if credits need to be returned to the player and then delete the bid.
There are a few other functions that were needed for the creation of the purchase tab, but I won't go into them in detail. Instead, I will post the full files for both
trade.js, including the functions for the sales tab.
The sales tab is mostly similar to the purchase tab, with a few key differences:
You can see examples of the differences in the screenshots below.
After all of that work, let's see how it all works together. The video below tests each of the interface elements discussed in this post.
If you liked this post and want to see more like it, let me know. Hopefully you'll have a deeper understanding of what it takes to create a game. Some parts are more exciting than others to code, but when it all comes together, it's extremely satisfying. If you have any questions about the post or the game in general, ask them in the comments below or connect via social media.
And one more reminder, there will be no update next week.Tweet comments powered by Disqus