PreludeSo, you have made your Tetris/Pong clone and now you have a brilliant idea for your next project! Almost invariably this is how a beginner game programmer starts out, and almost invariably that idea is to create a RPG or a MMORPG. (If this is not you, and you have no idea what I am talking about: MUD is Multi User Dungeon, RPG is Role Playing Game and MMORPG is Massively Multiplayer Online RPG) Well hold your horses. Let's not go leaping in. Chances are you have a great story line, or you have a brilliant battle engine worked out. You probably have this fantastic plan (read: delusion of grandeur) of thousands of people connecting to your game and making you millions of dollars. Well I'm sorry to be the one busting your bubble (and for newbie bashing), but I am afraid your plan isn't as great as you think. Chances are you do not have the ability to do all the content creation (music, artwork, models, etc) required for a game as large as an RPG, and chances are you probably don't have the complete programming ability to do it either. An RPG simply is not the next step after Tetris. But wait! Don't close the article, because I am here to help! Over this series I am going to show you how to make a MUD. It basically amounts to a cross between a chat server and a small RPG. All you will need to know is basic C++ and some Windows code. As an added bonus, I will be trying to keep it as extendable as possible, so that you can slowly evolve your server from a simple chat server to an advanced graphical multi-user universe. Topics I plan to cover over this series will include:
Just don't get ahead of yourself, though. First thing's first. In part one of this series I will be teaching you how to get a basic Windows application going with sockets. We will:
If you already know how to use Winsock, then you can probably skip this part of the series - although it may be useful to at least skim over the code. I will try and keep this one simple and explain as much as I can, including some coding techniques you may not know about. There are things that some of you may already understand fully, so bear with me. And finally, before we launch into it, a quick note about my code. I use Borland C++ Builder 3, so if you use MSVC you may have to change around a few things. For example, you may need to import libraries manually. It may be useful to have access to the Win32 SDK (including the Winsock2 API documentation) for this article, as I will not explain every API call in too much detail. For this particular article, I want to say a big thanks to Tim C. Schröder (tcs) who used to run glVelocity here at GameDev. Also a very special thanks to Matthew Daley (MattD), as large parts of code in this article are based off his code. You can download the source code here. Thomas "Cow In The Well" Cowell has been kind enough to change the source around so that it compiles under MSVC. This version should also compile under BCB. You can also download it here. The ApplicationMain FunctionOK, we are going to start by adding the basic application functionality. We are going to encapsulate Windows functionality in the class CApp. I will try to explain how to make a basic Windows application, but it is recommended that you know how to do this already. Create the class CApp and add constructor and deconstructor if you wish. Now we add the public function Main, which will be called directly from WinMain (the program entry point). It should look like this: class CApp { public: int Main(HINSTANCE hInstance, LPSTR lpCmdLine, int nCmdShow); }; Next we will create the body of the Main function. We will start with creating the window. WNDCLASSEX wcex; HWND hWnd; message window // Register the Windows class: wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = CS_HREDRAW | CS_VREDRAW; wcex.lpfnWndProc = (WNDPROC) GlobalWndProc; wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hInstance = hInstance; wcex.hIcon = NULL; wcex.hCursor = LoadCursor(NULL, IDC_ARROW); wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); wcex.lpszMenuName = NULL; wcex.lpszClassName = WINDOW_CLASS; wcex.hIconSm = NULL; if (!RegisterClassEx(&wcex)) return 0; // Create the Window hWnd = CreateWindowEx(NULL, WINDOW_CLASS, MUD_WINDOWTITLE, WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX, GetSystemMetrics(SM_CXSCREEN) / 2 - 300 / 2, GetSystemMetrics(SM_CYSCREEN) / 2 - 200 / 2, 300, 200, NULL, NULL, hInstance, this); if(!hWnd) return 0; ShowWindow(hWnd, SW_SHOW); UpdateWindow(hWnd); I am not going to explain that in too much detail, basically we are registering with Windows what we want our window to look like. You will notice the lack of data in the wcex structure; this will be a very simple window - I hate making interfaces in Windows. Next we actually create the window. hWnd is a handle to the window which will be used to refer to it. This is fairly obvious, just setting up appearance and sizes. The last argument is for user data. Later we will need to get a pointer to our CApp class back from Windows, so we use this. If you still don't understand how I did that, look up "WNDCLASSEX", "RegisterClassEx' and "CreateWindowEx" in the Win32 SDK. You will also need to define the following: #define MUD_WINDOWTITLE "Andrew Russell's MUD Server (v0.0)" #define WINDOW_CLASS "MUD_SRV" Last but not least, let's make it go into a message loop like so: // Go into the message loop MSG msg; while(GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return 1; This keeps getting messages until GetMessage returns false. When it returns false, it indicates that the program needs to end, so the function will go to return 1 and exit. Handling MessagesNow if you have programmed Windows before, you will know about the message system. Basically you register a callback function (using a function pointer). Windows will call that function giving it all sorts of different messages for it to process. This is how the application knows about what Windows is doing. Now, I am assuming you know about classes and how you can create multiple instances of a class. However you may not know about how function pointers work, particularly with classes. A function pointer is basically a pointer that, instead of pointing to data, points to a function to be executed. When you register your window with Windows you give it a pointer to the function that handles messages (look at wcex.lpfnWndProc above). If you need more info about function pointers, try your compiler help files. Now the problem that occurs with classes is that a function only exists once in memory, and that function or its function pointer does not have any information on which instance of the class it came from. Basically, a function used in a function pointer must be global (not in a class). There is a work around for this. The function must be declared static. If data is static, then it only exists once (no more, no less) no matter how many times you create its class. A static member function can only directly access static member data, as it is not bound to any particular instance of that class. This will be important when we get on to singletons. So now we add the static member function, GlobalWndProc to the public section. You will recall that this is what we set wcex.lpfnWndProc to point to. static LRESULT CALLBACK GlobalWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { LPCREATESTRUCT cs; if (message == WM_NCCREATE) { cs = (LPCREATESTRUCT) lParam; SetWindowLong(hWnd, GWL_USERDATA, (LONG) cs->lpCreateParams); } CApp *pApp = (CApp *) GetWindowLong(hWnd, GWL_USERDATA); return pApp->WndProc(hWnd, message, wParam, lParam); } If you have never made a Windows application before, never mind about all the funny data types, you can learn them from a proper Windows programming tutorial. Now you will notice that I have filled in the contents of this function. The first part will respond to a WM_NCCREATE message and take the this pointer that we put in the call to CreateWindowEx, and use the SetWindowLong function to transfer that pointer to the user data of the hWnd. The next section responds to every other message. Because the static function does not know about any particular instance of CApp, it gets the pointer to the instance we want from the window user data. Then it calls the specific WndProc function of the application. In the end the major benefit of this is that you can make all the other functions virtual and then inherit CApp and you will still call the correct version of WndProc. The other benefit is that we can keep everything nicely in a class. But of course, we actually have to write WndProc. Add it to the private section of CApp. private: LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); Again, don't worry if you don't know about the data types. Look them up in the SDK if you really need to know. And onto the body of the function: LRESULT CALLBACK CApp::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_CLOSE: // Quit the program PostQuitMessage(0); break; case WM_PAINT: PAINTSTRUCT ps; HDC hdc; hdc = BeginPaint(hWnd, &ps); RECT rt; GetClientRect(hWnd, &rt); DrawText(hdc, MUD_TITLE, strlen(MUD_TITLE), &rt, DT_CENTER); // Note: the folowing line will move the rectangle down so you can add more text // rt.top += 16; EndPaint(hWnd, &ps); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } return 0; } At the moment the function only processes two messages, the close message and the paint message. All other messages get sent through DefWindowProc, which means Default Window Process. This just defines the default behaviour. The close message here simply sends a quit message, but it doesn't have to. This is what is used for those "Are you sure you wish to close?" message boxes. The paint message is what we will add to throughout this tutorial to display important status in our window. This does not need to display much, as we will be using remote administration for pretty much everything. Note that you will also need the following define: #define MUD_TITLE "MUD Pies - Example server" Run itFinally, you have to create the actual WinMain function to call the Main function of CApp. WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { CApp Application; Application.Main(hInstance, lpCmdLine, nCmdShow); return 0; } At this point, just make sure you have all your includes in the right place. Make sure to include windows.h and winsock2.h before any calls to Windows or Winsock specific functions. If your compiler pukes, then you probably don't have your includes right. If your linker pukes, remember to have the lib files added correctly.
|
|