Thoughts on cc3k village operation
When I finished this assignment in object-oriented curriculum design, I haven't summarized my experience. Today, take the opportunity to answer the students' questions to summarize.
Before starting
The teacher asked to implement it in C + +, but on the other hand, I only knew this language at that time.
At that time, I thought a lot and read a lot in order to do this program. For example, the job requires that the final presentation form is a CLI interface program. Therefore, I tried to learn ncurses, but it didn't end in the end.
The main reference is the homework of some students found on the Internet. First of all, it can be seen from the wording of this assignment that it does not come from domestic mass universities. Therefore, the author tries to use the code hosting platform GitHub I found a lot of related projects, some of which contained the original PDF document of the assignment. I had the impression that it was an OOP assignment from the University of Waterloo, and the time limit was only about 3 weeks. The author has been working for nearly a semester and has to say that he is very ashamed.
After seeing the original homework documents, I found how broken and incomplete the content spread to domestic college teachers. Let's use assignment to call this job. As can be seen from the projects on the code hosting platform, this assignment has been used for more than one year, and the documents describing the assignment are different every year. The content of this assignment abroad is very detailed, describing the background of the topic, the specific requirements of the program, the input and output demonstration, the places that should be considered in the process of program design, and so on.
The background of the program is about this. There is a game Chamber Crawler 3000, CC3K for short. In this assignment, the villain wants to turn over and be the protagonist, so it is called CC3K village. The document also gives a specific and detailed definition of each element in the program, and also mentions that you can try to use Design Patterns for programming, such as strategy patterns and decoration patterns.
Worried about possible copyright issues, the author does not directly release the PDF file of assignment here. Students in need can go to GitHub for reference, such as This warehouse Medium PDF file.
outside the box
You can see many cc3k village projects implemented by others on the code hosting platform, which can be used as a reference before we start.
Better safe than sorry
Before we start writing code, we need to build an overall perception of the project. Although this is only a small program, I believe many students, like the author, have almost no clue about this topic at the beginning and don't know where to start. On the other hand, with sufficient preparation in advance, it is not easy to have the problem of being short of money and robbing Peter to pay Paul in the subsequent coding process.
In terms of idioms, we should be confident.
When I finished this assignment, because I didn't do enough design at the beginning, there are often places that need to modify the previous design in the design process. These changes often mean that other parts of the whole program need to be modified. With the development of the program, the code is more and more, and the modification is more and more complex.
From a professional point of view, we may need Software Engineering knowledge to analyze and design this project. Considering the complexity and stylization of professional analysis, the author will not strictly follow the design process guided by Software Engineering, but analyze it from the perspective of partial perceptual cognition.
Inferring internal logic from representation
We can imagine the program to be implemented according to the existing requirements. (however, the course instructor may also provide executable procedures for reference)
At the beginning of the program, players are required to choose an identity. After that, the player wanders on the "floor" with this identity; There may be monsters or items on the floor, and players can interact with monsters or items - that is, the program accepts the commands entered by the user and performs attacks, prop use or movement according to the commands. In other words, the program (data / entity in) changes in response to user input.
This game is actually a simplification of the real world. In real society, the interaction between objects is going on all the time, and the state of each entity in this program is updated only after the user operates. Specifically, after the user performs an operation (such as "attack", "use props", "move", etc.), other entities in the game are updated (such as "attack player" and "position move").
It can be seen that the main body of the game is a cycle of "user operation", "game update" and "waiting for user operation".
In the computer, we want to simulate the operation of the real world. We can divide time into several times, each time has a state, and define the conversion rules between states. In this way, the state of the next moment can be deduced from the state of this moment. In the CC3K job, you need to wait for user input between two times. If the user does not operate, the time will not start to flow. (however, in the real world, other entities will not stop interacting with each other because of the stagnation of the protagonist.)
However, we have to consider the output of the program. The above process is nothing more than updating the data in the computer according to the algorithm, but users prefer to see an intuitive representation. In this operation, that is, a "map" built by characters, which shows the player's location and various entities and elements on the floor.
When we add the output, the game becomes a cycle of "displaying game status", "waiting for user operation", "user operation" and "game update".
Modeling solids
With the above knowledge, we need to model the entities involved in the program. Specifically, what kind of data field s are used to describe them.
In terms of object-oriented programming, it is to establish corresponding classes for these entities. The data member declaration in the class corresponds to the field describing the attribute of the class; Class, which defines how this object interacts with the outside world (other objects).
Therefore, we first need to observe which entities are designed in the program.
The first thought may be characters and items. For a character, we need to record its health value, location and other information. For an item, we need to record its attribute (effect after use), location and other information.
When we talk about location, we have to talk about terrain. Terrain will determine the generation of items and the movement of characters. In CC3K, the terrain is reflected as the channel between the room and the connecting room.
Next, we need to consider how to store this information.
First consider terrain output. The target output of our program is a text interface, that is, in this game, the terrain can be regarded as composed of adjacent square grids, and each grid can be a floor, wall or channel. Therefore, from an intuitive point of view, we can use the "two-dimensional array" in the computer to store these 25 × 79 squares.
After that, we need to establish the relationship between the character and the map. In other words, we need to display the character on the map.
Readers may think that a symbol can be directly used to represent a role and stored in terrain data. But the problem is that a certain type of game may have several entities, so it can't be generalized with simple symbols. That is, we need to associate the markers representing entities on the terrain with specific objects.
Some students have adopted such a method at this time, that is, a Cell class is defined, and the terrain is defined by 25 × It consists of 79 Cell objects and stores entities in these Cell objects in some form. For example, keep a pointer in the Cell object. If it is not empty, it indicates that it points to a specific object.
In this way, if you want to output the map, you only need to traverse all Cell objects (output while traversing) to easily display the whole map (you can find the objects at the corresponding position directly according to the terrain, so as to). Moreover, it is also convenient to find the adjacency relationship between entities. You only need to check the adjacent Cell objects.
This idea is very direct and easy to implement, but there is a problem that needs attention, that is, the change of roles (such as moving, generating or disappearing) should be applied to the map; On the contrary, the changes on the map should also be applied to the role; If you accidentally forget to update the information on the map, you will make an error.
Therefore, the author used another way to store the terrain and objects separately, but not using "pointer", but using "coordinates" to connect the two. We found that selecting a coordinate datum point on the terrain can represent the position of the character, that is, the relative relationship between the object and the terrain is linked by coordinates. Compared with the previous method, this method can not judge the adjacency relationship by the relative position of the data stored in the computer memory (that is, it can not directly obtain whether there is an object on the terrain at a certain position or the specific information of the object), but by comparing the coordinate relationship with all the stored objects one by one. Similarly, when moving, you also need to get the coordinates of the character first, and then look up the terrain information to judge whether it can move.
In addition, if the data stored in this way is to be displayed, the result can not be obtained by traversing once in the above method. Instead, every time a location is accessed, you need to find out whether there is a role in the role array. If so, output the corresponding symbol; otherwise, output the terrain of the corresponding location. It can be seen that such a program will execute many more instructions. Therefore, we can introduce a concept of "canvas": first draw the terrain on the "canvas", then traverse all entities, and then draw the entities on the output topographic map.
Although this method is troublesome, it has some benefits in the long run. Firstly, this form is not limited to two-dimensional grid, and secondly, it provides an idea for the storage of game data. We know that the physical location of the object in memory is not necessarily the same every time the program runs. Therefore, when we record the game state, we must use a way independent of the program operation to record the relationship between terrain and characters. It's easy to think of coordinate representation.
Process modeling
Next, we will design methods for these entities.
In CC3K, the interaction between different roles is different. That is, a role is attacked by different roles and has different coping styles.
According to object-oriented learning, we can first think of designing different types and defining methods for different types for each type. From the perspective of convenient management, we need to derive these types from a base class. So how to determine the type from a base class pointer? In fact, if we just implement different policies when dealing with different types, we can use "Visitor Pattern".
Reference link: https://stackoverflow.com/questions/17678913/know-the-class-of-a-subclass-in-c
Using the virtual method of C + +, you can access the corresponding method of the specific class through the base class pointer. The following example is an example of a visitor pattern.
In the example, AnimalHitter's beating of animals is only an example, and no animals were hurt during the demonstration.
#ifndef ANIMAL_HITTER_HPP #define ANIMAL_HITTER_HPP class Animal; class Cat; class Dog; class AnimalHitter { public: virtual void hit(Cat *) = 0; // or: void hitCat(Animal *); virtual void hit(Dog *) = 0; // or: void hitDog(Animal *); virtual void be_biten(Animal * p) = 0; virtual ~AnimalHitter() = default; }; class Human : public AnimalHitter { public: void hit(Cat * p) override; void hit(Dog * p) override; void be_biten(Animal * p) override; ~Human() = default; }; // Since Cat and Dog have not been specifically defined, they belong to "incomplete type", // Therefore, functions cannot be defined directly in the inline mode in the Human class, // You need to define a function outside the class #endif // ANIMAL_HITTER_HPP
#ifndef ANIMAL_HPP #define ANIMAL_HPP #include "AnimalHitter.hpp" #include <iostream> class Animal { public: virtual void be_hit_by(AnimalHitter *) = 0; virtual void bite(Human *) = 0; virtual ~Animal() = default; }; class Dog : public Animal { public: void be_hit_by(AnimalHitter * hitter) override { hitter->hit(this); // match AnimalHitter::hit(Dog*) // or call AnimalHitter::hitDog(Animal *) std::cout << "Dog: Woof, woof!" << std::endl; } void bite(Human * human) override { std::cout << "Dog: [bite human]" << std::endl; } ~Dog() = default; }; class Cat : public Animal { public: void be_hit_by(AnimalHitter * hitter) override { hitter->hit(this); // match AnimalHitter::hit(Cat*) // or call AnimalHitter::hitCat(Animal *) std::cout << "Cat: Meow!" << std::endl; } void bite(Human * human) override { std::cout << "Cat: [bite human]" << std::endl; } ~Cat() = default; }; #endif // ANIMAL_HPP
#include "AnimalHitter.hpp" #include "Animal.hpp" // In fact, you only need to reference an Animal.hpp header file void Human::hit(Cat * cat) { std::cout << "Human: [hit cat] I hit a cat!" << std::endl; } void Human::hit(Dog * dog) { std::cout << "Human: [hit dog] I hit a dog!" << std::endl; } void Human::be_biten(Animal * animal) { animal->bite(this); std::cout << "Human: Ouch!" << std::endl; }
#include "Animal.hpp" #include "AnimalHitter.hpp" int main() { Animal* animals[2] = { new Cat(), new Dog() }; AnimalHitter* human = new Human(); for (auto animal : animals) { std::cout << "---------\n"; std::cout << "The human is going to hit an animal.\n"; animal->be_hit_by(human); std::cout << "An animal is going to bite the human.\n"; human->be_biten(animal); std::cout << "---------" << std::endl; } for (auto p : animals) { delete p; } delete human; }
add_executable(demo "Animal.hpp" "AnimalHitter.hpp" "AnimalHitter.cpp" "demo.cpp")
As above, you can write the relationship between characters in the game in the same way. In addition, the method of "identifying the type by adding a label field to the type" can also be adopted.
After that, there is a simple game framework demonstration.
#ifndef PLAYER_HPP #define PLAYER_HPP struct Point { int row; int col; }; class Player { private: Point pos; public: Player() {} auto set_pos(Point pos_) { this->pos = pos_; } auto get_pos() { return this->pos; } auto get_denote() { return '@'; } ~Player() {} }; #endif
#ifndef FLOOR_HPP #define FLOOR_HPP #include "Player.hpp" #include <string> #include <vector> #include <iostream> class Floor { private: static const int width = 10; static const int height = 5; std::vector<std::string> terrian; // There is only one protagonist at present Player pc; // If you have multiple other roles, you can use container storage, such as // std::vector<Character> characters; public: Floor(); ~Floor() = default; bool is_avaliable_pos(Point pos) { return terrian[pos.row][pos.col] == '.'; } void move_player(Point new_pos) { pc.set_pos(new_pos); } auto get_player_pos() { return pc.get_pos(); } void print_to(std::ostream &os); }; #endif // FLOOR_HPP
#include "Floor.hpp" Floor::Floor() { // Function to generate room terrain terrian = { "|--------|", "|........|", "|........|", "|........|", "|--------|"}; // Set player position pc.set_pos({3, 4}); } void Floor::print_to(std::ostream & os) { // Output map char canvas[height][width]; // Canvas, which acts as a buffer // Draw the terrain first for (int i = 0; i < height; ++i) { for (int j = 0; j < width; ++j) { canvas[i][j] = terrian[i][j]; } } os << "\033[H\033[J"; // Move the cursor of the terminal to the upper left and output it. The effect is about equal to screen clearing // Draw the character on the canvas canvas[pc.get_pos().row][pc.get_pos().col] = pc.get_denote(); // If there are multiple roles, this is to use a loop to traverse the role list // Output the content on the canvas for (int i = 0; i < height; ++i) { for (int j = 0; j < width; ++j) { os << canvas[i][j]; } os << "\n"; } // flush before subsequent operations to ensure that the output is displayed in time os << std::flush; }
#include <iostream> #include <string> #include "Floor.hpp" int main() { // General control process Floor floor; std::string input; while (true) { // Displays the current status floor.print_to(std::cout); // Waiting for user input std::getline(std::cin, input); if (input == "#") break; // Process user input auto newPos = floor.get_player_pos(); if (input == "w") { newPos.row -= 1; } if (input == "a") { newPos.col -= 1; } if (input == "s") { newPos.row += 1; } if (input == "d") { newPos.col += 1; } if (floor.is_avaliable_pos(newPos)) { floor.move_player(newPos); } // Game status updates, such as the movement of entities in a room } }
After saving the code as the corresponding file, use the CMakeLists.txt configuration project below to run.
add_executable(demo "Animal.hpp" "AnimalHitter.hpp" "AnimalHitter.cpp" "demo.cpp") add_executable(rogue-mini "Player.hpp" "Floor.hpp" "Floor.cpp" "rogue-mini.cpp")