Category Archives: Gamedev

SDL 2.0 Tutorial-06: Networking 3

In the last tutorial we setup a basic TCP server. In this tutorial we will start by modifying the server we setup and finish by writing a player client application. In order to write a client application with SDL 2.0 we will need a lot of the ideas which where presented in my first string of SDL tutorials (here). A lot of my personal preferences for setting up SDL have changed since writing those tutorials so instead of using code found within those tutorials I will be using a collection of C++ files I have settled into using (found here).

First let’s start with a few server-side modifications. Let’s start by adding a few extra packet flags:

//-----------------------------------------------------------------------------
#define FLAG_QUIT 0x0000
#define FLAG_WOOD_QUEST 0x0011
#define FLAG_WOOD_UPDATE 0x0010
#define FLAG_WOOD_GETTIME 0x0012

Now we have a packet flag for starting and completing a quest, as well as a packet flag for getting the amount of time left until the quest is completed. Next we will update our server-side “RecvData” function:

uint8_t* RecvData(int index, uint16_t* length) {
	uint8_t temp_data[MAX_PACKET];
	int num_recv = SDLNet_TCP_Recv(sockets[index], temp_data, MAX_PACKET);

	if(num_recv <= 0) {
		CloseSocket(index);

		const char* err = SDLNet_GetError();
		if(strlen(err) == 0) {
			printf("DB: client disconnectedn");
		} else {
			fprintf(stderr, "ER: SDLNet_TCP_Recv: %sn", err);
		}

		return NULL;
	} else {
		*length = num_recv;

		uint8_t* data = (uint8_t*) malloc(num_recv*sizeof(uint8_t));
		memcpy(data, temp_data, num_recv);

		return data;
	}
}

What I have done here is removed the work this function was doing to grab the packet flag from the start of the incoming socket data. This is because when a socket is ready for us to process it may have several packets all lined together. Therefore the data buffer returned by “RecvData” may contain several packets depending on how quickly the client is sending packets to us; so any work “RecvData” does to grab the flag from the first packet doesn’t help us identify the packet flags for all the other packets possibly following the first. Therefore the extra work being done isn’t necessary at this level.

Next we will add a new function called “ProcessData.” This function will continuously run on each packet we find within the data buffer returned by “RecvData.”

void ProcessData(int index, uint8_t* data, uint16_t* offset) {
	if(data == NULL) return;

	uint16_t flag = *(uint16_t*) &data[*offset];
	*offset += sizeof(uint16_t);

	switch(flag) {
		case FLAG_WOOD_UPDATE: {
			uint16_t send_offset = 0;
			uint8_t send_data[MAX_PACKET];

			memcpy(send_data+send_offset, &clients[index].amt_wood, sizeof(uint8_t));
			send_offset += sizeof(uint8_t);

			SendData(index, send_data, send_offset, FLAG_WOOD_UPDATE);
		} break;

		case FLAG_WOOD_GETTIME: {
			uint16_t send_offset = 0;
			uint8_t send_data[MAX_PACKET];

			uint32_t time_left;
			if(clients[index].questing) {
				time_left = WOOD_WAIT_TIME-(SDL_GetTicks()-clients[index].timer_wood);
			} else time_left = 0;

			memcpy(send_data+send_offset, &time_left, sizeof(uint32_t));
			send_offset += sizeof(uint32_t);

			SendData(index, send_data, send_offset, FLAG_WOOD_GETTIME);
		} break;

		case FLAG_WOOD_QUEST: {
			if(!clients[index].questing) {
				clients[index].questing = 1;
				clients[index].timer_wood = SDL_GetTicks();
			}
		} break;

		case FLAG_QUIT: {
			running = 0;
			printf("DB: shutdown by client id: %dn", index);
		} break;
	}
}

This is where we do the work and grab the packet’s flag. Everything here should look similar to what we where doing before when the server found client sockets which were ready with information to be processed – except for the fact that we have added two extra flags. The “GETTIME” flag checks if the player is currently questing; if not then the time left on the quest is 0, otherwise the server sends the time left on the quest to the player. The other flag is the “QUEST” flag; this flag checks whether the client is currently questing and starts the quest if they aren’t currently in a quest.

The finial server-side update we make will be to the main processing loop. First let’s crank up how often we check for ready sockets:

int num_rdy = SDLNet_CheckSockets(socket_set, 50);

If num_rdy <= 0 then we do not have any sockets ready for processing so we enter the server’s idle tasks. Previously we were simply adding four wood to the client’s resource count continuously. This is quite a silly thing to do! Instead let’s check for whether a client has completed any quests and if so we’ll send out a quest completed packet and a wood update packet. In addition, we’ll continuously send out “GETTIME” packets to keep the client updated with the server’s timer.

int ind;
for(ind=0; ind<MAX_SOCKETS; ++ind) {
	if(!clients[ind].in_use) continue;

	if(clients[ind].questing &&
		(SDL_GetTicks()-clients[ind].timer_wood)>WOOD_WAIT_TIME
	) {
		clients[ind].questing = 0;
		clients[ind].amt_wood += 4;
		SendData(ind, NULL, 0, FLAG_WOOD_QUEST);

		uint16_t send_offset = 0;
		uint8_t send_data[MAX_PACKET];

		memcpy(send_data+send_offset, &clients[ind].amt_wood, sizeof(uint8_t));
		send_offset += sizeof(uint8_t);

		SendData(ind, send_data, send_offset, FLAG_WOOD_UPDATE);
	}

	uint16_t send_offset = 0;
	uint8_t send_data[MAX_PACKET];

	uint32_t time_left;
	if(clients[ind].questing) {
		time_left = WOOD_WAIT_TIME-(SDL_GetTicks()-clients[ind].timer_wood);
	} else time_left = 0;

	memcpy(send_data+send_offset, &time_left, sizeof(uint32_t));
	send_offset += sizeof(uint32_t);

	SendData(ind, send_data, send_offset, FLAG_WOOD_GETTIME);
}

And for the final server touch we’ll update the code which runs if there are sockets ready to be processed. Notice that how we handle the server socket (for new connections) isn’t changing but how we handle the client sockets is changing.

if(SDLNet_SocketReady(server_socket)) {
	int got_socket = AcceptSocket(next_ind);
	if(!got_socket) {
		num_rdy--;
		continue;
	}

	// NOTE: get a new index
	int chk_count;
	for(chk_count=0; chk_count<MAX_SOCKETS; ++chk_count) {
		if(sockets[(next_ind+chk_count)%MAX_SOCKETS] == NULL) break;
	}

	next_ind = (next_ind+chk_count)%MAX_SOCKETS;
	printf("DB: new connection (next_ind = %d)n", next_ind);

	num_rdy--;
}

int ind;
for(ind=0; (ind<MAX_SOCKETS) && num_rdy; ++ind) {
	if(sockets[ind] == NULL) continue;
	if(!SDLNet_SocketReady(sockets[ind])) continue;

	uint8_t* data;
	uint16_t length;
				
	data = RecvData(ind, &length);
	if(data == NULL) {
		num_rdy--;
		continue;
	}

	int num_processed = 0;

	uint16_t offset = 0;
	while(offset < length) {
		num_processed++;
		ProcessData(ind, data, &offset);
	}

	printf("num_processed from ID: %d was %dn", ind, num_processed);
	fflush(stdout);

	free(data);
	num_rdy--;
}

I am also keeping track of the number of packets which the server processes for each of the ready clients for debugging purposes. Now the client-side stuff isn’t that different when it comes to the network side of things. A lot of the helper functions we have server-side will have analogous client-server versions. Here is the client-side code in its entirety:

/*
*/

#include "AdBase.h"
#include "AdLevel.h"
#include "AdScreen.h"
#include "AdSpriteManager.h"

//-----------------------------------------------------------------------------
#define MAX_PACKET 0xFF

//-----------------------------------------------------------------------------
#define FLAG_QUIT 0x0000
#define FLAG_WOOD_QUEST 0x0011
#define FLAG_WOOD_UPDATE 0x0010
#define FLAG_WOOD_GETTIME 0x0012

//-----------------------------------------------------------------------------
TCPsocket socket;
SDLNet_SocketSet socket_set;

//-----------------------------------------------------------------------------
int questing;
uint8_t amt_wood;
uint32_t timer_wood;

//-----------------------------------------------------------------------------
void CloseSocket(void) {
	if(SDLNet_TCP_DelSocket(socket_set, socket) == -1) {
		fprintf(stderr, "%sn", SDLNet_GetError());
		system("pause");
		exit(-1);
	}

	SDLNet_FreeSocketSet(socket_set);
	SDLNet_TCP_Close(socket);
}

//-----------------------------------------------------------------------------
void SendData(uint8_t* data, uint16_t length, uint16_t flag) {
	uint8_t temp_data[MAX_PACKET];

	int offset = 0;
	memcpy(temp_data+offset, &flag, sizeof(uint16_t));
	offset += sizeof(uint16_t);
	memcpy(temp_data+offset, data, length);
	offset += length;

	int num_sent = SDLNet_TCP_Send(socket, temp_data, offset);
	if(num_sent < offset) {
		fprintf(stderr, "ER: SDLNet_TCP_Send: %sn", SDLNet_GetError());
		CloseSocket();
	}
}

//-----------------------------------------------------------------------------
uint8_t* RecvData(uint16_t* length) {
	uint8_t temp_data[MAX_PACKET];
	int num_recv = SDLNet_TCP_Recv(socket, temp_data, MAX_PACKET);

	if(num_recv <= 0) {
		CloseSocket();

		const char* err = SDLNet_GetError();
		if(strlen(err) == 0) {
			printf("DB: server shutdownn");
		} else {
			fprintf(stderr, "ER: SDLNet_TCP_Recv: %sn", err);
		}

		return NULL;
	} else {
		*length = num_recv;

		uint8_t* data = (uint8_t*) malloc(num_recv*sizeof(uint8_t));
		memcpy(data, temp_data, num_recv);

		return data;
	}
}

//-----------------------------------------------------------------------------
void ProcessData(uint8_t* data, uint16_t* offset) {
	if(data == NULL) return;

	uint16_t flag = *(uint16_t*) &data[*offset];
	*offset += sizeof(uint16_t);

	switch(flag) {
		case FLAG_WOOD_UPDATE: {
			amt_wood = *(uint8_t*) &data[*offset];
			*offset += sizeof(uint8_t);
		} break;

		case FLAG_WOOD_GETTIME: {
			timer_wood = *(uint32_t*) &data[*offset];
			*offset += sizeof(uint32_t);
		} break;

		case FLAG_WOOD_QUEST: {
			// NOTE: quest completed
			questing = 0;
		} break;
	}
}

//-----------------------------------------------------------------------------
void InitNetwork(const char* pIP, int iPort) {
	IPaddress ip;
	if(SDLNet_ResolveHost(&ip, pIP, iPort) == -1) {
		fprintf(stderr, "%sn", SDLNet_GetError());
		system("pause");
		exit(-1);
	}

	socket = SDLNet_TCP_Open(&ip);
	if(socket == NULL) {
		fprintf(stderr, "%sn", SDLNet_GetError());
		system("pause");
		exit(-1);
	}

	socket_set = SDLNet_AllocSocketSet(1);
	if(socket_set == NULL) {
		fprintf(stderr, "%sn", SDLNet_GetError());
		system("pause");
		exit(-1);
	}

	if(SDLNet_TCP_AddSocket(socket_set, socket) == -1) {
		fprintf(stderr, "%sn", SDLNet_GetError());
		system("pause");
		exit(-1);
	}
}

//-----------------------------------------------------------------------------
bool CheckSocket(void) {
	if(SDLNet_CheckSockets(socket_set, 0) == -1) {
		fprintf(stderr, "%sn", SDLNet_GetError());
		system("pause");
		exit(-1);
	}

	return SDLNet_SocketReady(socket);
}

//-----------------------------------------------------------------------------
int SDL_main(int argc, char* argv[]) {
	if(AdBase::Init(8*40, 8*30, 3) == false) {
		fprintf(stderr, "ERROR: Failed to initiate.n");
		system("pause");
		return -1;
	}

	// TESTING
	InitNetwork("PUT_SERVER_IP_HERE!!!", 8099); // arg #2 is the port

	AdLevel* testLvl = new AdLevel();
	//

	SDL_Event sdlEvent = {};
	while(sdlEvent.type != SDL_QUIT) {
		SDL_PollEvent(&sdlEvent);

		AdScreen::Clear();

		// TESTING
		if(CheckSocket()) {
			uint16_t length, flag;
			uint8_t* data = RecvData(&length);

			uint16_t offset = 0;
			while(offset < length) {
				ProcessData(data, &offset);
			}

			free(data);
		}
		//

		// TESTING
		testLvl->Update(&sdlEvent);

		char string[0xFF];
		sprintf(string, "Wood: %d", amt_wood);

		SDL_Point pnt1 = {0, 0};
		SDL_Color color1 = {0xFF, 0x00, 0x00, 0x00};
		SDL_Surface* pSurf = AdSpriteManager::BuildSprite(string, color1);
		AdScreen::DrawSprite(pnt1, pSurf);
		SDL_FreeSurface(pSurf);

		pnt1.y += 8;
		sprintf(string, "Timer: %d", (int) ceil((double)timer_wood/1000.0f));
		pSurf = AdSpriteManager::BuildSprite(string, color1);
		AdScreen::DrawSprite(pnt1, pSurf);
		SDL_FreeSurface(pSurf);
		//

		//
		if(timer_wood == 0) {
			SDL_Point pnt2 = {128, 8};
			SDL_Rect rec2 = {pnt2.x, pnt2.y, 5*8, 8};

			if(
				testLvl->m_iMouseX>=rec2.x && testLvl->m_iMouseX<=(rec2.x+rec2.w) &&
				testLvl->m_iMouseY>=rec2.y && testLvl->m_iMouseY<=(rec2.y+rec2.h)
			) {
				SDL_Color color2 = {0x00, 0xFF, 0x00, 0x00};
				pSurf = AdSpriteManager::BuildSprite("Quest", color2);

				if(testLvl->m_bMouseLeft && !questing) {
					questing = 1;
					SendData(NULL, 0, FLAG_WOOD_QUEST);
				}
			} else {
				SDL_Color color2 = {0x00, 0x00, 0xFF, 0x00};
				pSurf = AdSpriteManager::BuildSprite("Quest", color2);
			}
			
			AdScreen::DrawSprite(pnt2, pSurf);
			SDL_FreeSurface(pSurf);
		}
		//

		AdScreen::Present();
	}

	// TESTING
	delete testLvl;

	SendData(NULL, 0, FLAG_QUIT);
	CloseSocket();
	//

	AdBase::Quit();

	return 0;
}

All the SDL graphics processing and input handling is being wrapped with the custom files I wrote and linked at the beginning of this post. If you find these posts helpful or would like me to dive deeper into a particular topic covered here please send me an email at “info@stephenmeier.net”.

SDL 2.0 Tutorial-05: Networking 2

In the last tutorial we finished with a hallow if-statement within our processing loop.

int running = 1;
while(running) {
	int num_rdy = SDLNet_CheckSockets(socket_set, 1000);

	if(num_rdy <= 0) {
		// NOTE: none of the sockets are ready
	} else {
		// NOTE: some number of the sockets are ready
	}
}

If there aren’t any sockets which we need to process (i.e. num_rdy <= 0 ) then we’ll loop over all the currently connected client sockets and perform any tasks which may be pending. This is basically the server’s chance to catch up with game state and perform any security checks. This will work well for a simple TCP game where the server and client need not be in sync the entire time. For something more real-time we would prioritize game state and perhaps use UDP instead.

For now let’s add four wood to the resource count for each connected client.

int ind;
for(ind=0; ind<MAX_SOCKETS; ++ind) {
	if(!clients[ind].in_use) continue;
	clients[ind].amt_wood += 4;
}

Remember this will run every second assuming there aren’t any sockets to be processed (i.e. num_rdy <= 0 ). Why am I saying it’ll run every second? It is because of the following function call:

SDLNet_CheckSockets(socket_set, 1000);

The second argument to this function is 1000, which is the number of milliseconds this function will block while waiting for a socket connection.

Now, if there are sockets ready to be processed we’ll have to check whether it is a client socket or the server socket. If the server socket is ready then we likely have a new client connection. For the client connections we’ll have to check each one until we find the ones which are ready and then process the data that we have received from them.

First let’s handle the case when the server socket is ready:

if(SDLNet_SocketReady(server_socket)) {
	int got_socket = AcceptSocket(next_ind);
	if(!got_socket) {
		num_rdy--;
		continue;
	}

	// NOTE: get a new index
	int chk_count;
	for(chk_count=0; chk_count<MAX_SOCKETS; ++chk_count) {
		if(sockets[(next_ind+chk_count)%MAX_SOCKETS] == NULL) break;
	}

	next_ind = (next_ind+chk_count)%MAX_SOCKETS;
	printf("DB: new connection (next_ind = %d)n", next_ind);

	num_rdy--;
}

Here the function int AcceptSocket(int index); is a shorthand for accepting a new socket connection using sockets[index] and clients[index] . This function returns whether or not the connection was successfully accepted. Here is the function itself:

int AcceptSocket(int index) {
	if(sockets[index]) {
		fprintf(stderr, "ER: Overriding socket at index %d.n", index);
		CloseSocket(index);
	}

	sockets[index] = SDLNet_TCP_Accept(server_socket);
	if(sockets[index] == NULL) return 0;

	clients[index].in_use = 1;
	if(SDLNet_TCP_AddSocket(socket_set, sockets[index]) == -1) {
		fprintf(stderr, "ER: SDLNet_TCP_AddSocket: %sn", SDLNet_GetError());
		exit(-1);
	}

	return 1;
}

The function void CloseSocket(int index); was presented in the previous part of this tutorial. After we accept the new client connection we also update the next_ind variable so that we are ready for the next client connection. As it’s currently written, if a new client connects when the server is full then another client will be kicked to make room for the new connection.

Now let’s process any clients which might have data for us.

int ind;
for(ind=0; (ind<MAX_SOCKETS) && num_rdy; ++ind) {
	if(sockets[ind] == NULL) continue;
	if(!SDLNet_SocketReady(sockets[ind])) continue;

	uint8_t* data;
	uint16_t flag;
	uint16_t length;
				
	data = RecvData(ind, &length, &flag);
	if(data == NULL) {
		num_rdy--;
		continue;
	}

	switch(flag) {
		case FLAG_WOOD_UPDATE: {
			uint16_t offset = 0;
			uint8_t send_data[MAX_PACKET];

			memcpy(send_data+offset, &clients[ind].amt_wood, sizeof(uint8_t));
			offset += sizeof(uint8_t);

			SendData(ind, send_data, offset, FLAG_WOOD_UPDATE);
		} break;

		case FLAG_QUIT: {
			running = 0;
			printf("DB: shutdown by client id: %dn", ind);
		} break;
	}

	free(data);
	num_rdy--;
}

What we are doing here is looping over all the client sockets and checking to see whether a client is connected and whether that connected client has some information ready for us. We also want to make sure that we aren’t working harder than we have to by breaking out of the for-loop when num_rdy == 0 ; because at that point we have processed all the ready client sockets so there is no point in checking any remaining client connections for information.

Once we know that we have a connected socket and that the socket have information ready for us, we fill a buffer with the data they sent us. This is what the uint8_t* RecvData(int index, uint16_t* length, uint16_t* flag); function does for us:

uint8_t* RecvData(int index, uint16_t* length, uint16_t* flag) {
	uint8_t temp_data[MAX_PACKET];
	int num_recv = SDLNet_TCP_Recv(sockets[index], temp_data, MAX_PACKET);

	if(num_recv <= 0) {
		CloseSocket(index);
		const char* err = SDLNet_GetError();
		if(strlen(err) == 0) {
			printf("DB: client disconnectedn");
		} else {
			fprintf(stderr, "ER: SDLNet_TCP_Recv: %sn", err);
		}

		return NULL;
	} else {
		int offset = 0;
		*flag = *(uint16_t*) &temp_data[offset];
		offset += sizeof(uint16_t);

		*length = (num_recv-offset);

		uint8_t* data = (uint8_t*) malloc((num_recv-offset)*sizeof(uint8_t));
		memcpy(data, &temp_data[offset], (num_recv-offset));

		return data;
	}
}

This function gets data from sockets[index] and returns a pointer to a new buffer. The in/out function arguments length and flag are the length of the newly allocated buffer and the packet flag for the information we are receiving from the client.

If the client sends us a request for the amount of wood they current possess (i.e. FLAG_WOOD_UPDATE ) then we reply by sending them a packet with the information they are requesting. To send information to the connected client we use the following shorthand function:

void SendData(int index, uint8_t* data, uint16_t length, uint16_t flag) {
	uint8_t temp_data[MAX_PACKET];

	int offset = 0;
	memcpy(temp_data+offset, &flag, sizeof(uint16_t));
	offset += sizeof(uint16_t);
	memcpy(temp_data+offset, data, length);
	offset += length;

	int num_sent = SDLNet_TCP_Send(sockets[index], temp_data, offset);
	if(num_sent < offset) {
		fprintf(stderr, "ER: SDLNet_TCP_Send: %sn", SDLNet_GetError());
		CloseSocket(index);
	}
}

For debugging purposes the client can also shutdown the server with a FLAG_QUIT . In the next tutorial we will write a simple client which will display the amount of wood the client current has. This amount will continue to increase because the server currently adds four wood each time it enters idle processing. Once we have achieved this we’ll add a “quest” button and timer on the client-side and a few more packet flags allowing the client to request the amount of time left on a quest and to start a quest. The server will check for when a quest completes and will increase the amount of wood for that client as a result.

Till next time!

Current Projects

I have been really busy with work recently but I’m making some time to discuss my current hobby projects. One of these project is more of a solo gig and the other, I’m hoping, will start with me doing most of the work at first but eventually I’d like other developers to join in and help.

This first project is similar in style to Arthur’s Grove (AG). AG was a little game I released not too long ago but this time I’m stepping up my game a little more now that I’m more comfortable with a lot of the game mechanics I used when developing AG. The worlds are completely different and the stories have nothing to do with each other (not that AG had much of a story anyways) but the game itself will be similar in style because I find myself gravitating towards this style of game a lot.

The tech is also a bit different. Here I’m relying more on the Tiled Map Editor to set collision boundaries and add entity objects into the level. The Tiled Map Editor exports to JSON which I then parse using Duktape. I have been using Duktape for a while and anytime I need to do a bit of scripting on parse Javascript/JSON it is my go-to library!

The second project has recently grown in importance to me since, apparently, Snoxd has been down for whatever reason (most likely due to the lack of contributing community members). As much as I would like to see Snoxd succeed, my feeling is that the private KO server community needs strong developers and not a bunch of “binary hackers.” A community that doesn’t share will never grow and a community that doesn’t support and praise good contributions will never provide the positive reinforcement necessary to keep people contributing. Sometime in the future I’ll write up a more complete article on my feeling towards the KO development community and a direction for where I would like to see the community head towards in the future. But nevertheless, I have been working on a completely new implementation of the KO AIServer architecture using SDL2 and MySQL server. Currently I’m able to spawn in a group of NPCs with this custom code and have the AIServer communicate with the stock 1298 Game server which allows the player to login and see the NPCs. Most of the work here comes from re-implementing the multi-threaded nature of the AIserver using SDL2, along with all the network calls using SDL2 Net. This code is completely cross-platform and will compile on Windows/Mac/Linux.

One very important reason for using MySQL server instead of Microsoft’s SQL server (which the official USKO, as well as all private servers, use) is because if you were to compile the KO servers on Linux you would will need access to a Microsoft SQL server on another remote windows machine. Therefore, no matter what, a machine running Windows is necessary. However, if the database is a MySQL database this isn’t an issue as MySQL can be installed on any prominent operating system. Hell, even a custom coded server running Sqlite would provide more versatility than SQL server (but I digress).

So that’s the sort of stuff I’ve been working with recently. If you have any questions, or would like to help contribute to the OpenKO project, let me know. The long-term idea is to replace each of the KO game servers for the stock 1298 release one at a time. Once this is complete and the binaries are virtually interchangeable with the originally released 1298 private server binaries we will start writing a custom 1298 client using SDL2 and OpenGL. This is the tentative plan, and likely won’t recent full attention until I have finished my PhD and have find a job. But the progress made so far is still quite an accomplishment relative to the stagnation which unfortunately permeates the KO development scene. This will eventually change though because I know there are passionate developer out there and I would love to help be apart of that change.

New Project – “June”

So for whatever reason I recently went on a Youtube binge watching a ton of videos on game design and artistic design. Based on the videos I saw I feel motivated to mix things up a little and try something more creative. There are a ton of point and click adventure games out there but what I have planned is a bit more personal. I am prototyping the idea of turning my own day-to-day experiences into a point and click.

For this game I am also adding music which a lot of my previous projects have been missing, and all the images for the game were taken with my android phone. Music plays a much bigger role in this game because there aren’t really any “game mechanics”. The game mechanic is the experience and how well it draws the play in. Everything I have now has been thrown together quickly just to get a feel for the gameplay, and I’d have to say I like what the prototype has had to offer so far. The images are a bit blurry but linking image to image through an option list of activities is a lot of fun and has opened more of my creative side than any other project so far.

So I will keep with this for a while and see where it takes me!

NPC Questing

Here lately I have had some time to work on Arthur’s Grove (I’m still note 100% sold on the name, but we’ll see!). I have started to add a few basic “quests.” These mostly involve item fetching – for example, before you acquire a weapon you must fetch an item for a NPC. Even from these very basic quests characteristics of an NPC start to emerge. With the fetch quests, the player learns a lot about what the NPC desires. Some game characters might strive for coins or treasure, while others ask for family trinkets which have been recently lost or misplaced. These characteristics are nice because they are simple and easy to identify but also help the player start to build expectations for how a particular NPC will act when placed in a certain situation.

From these very simple to build quests the player starts to associate with each of these characters. Aligning with those they favor and disliking those characters they find it hard to identify with. Next task for me will be to start using these characters and what the play learns about them to start building elements of lore which persist throughout the game!