YOMEDIA
ADSENSE
Microsoft XNA Game Studio Creator’s Guide- P18
72
lượt xem 7
download
lượt xem 7
download
Download
Vui lòng tải xuống để xem tài liệu đầy đủ
Microsoft XNA Game Studio Creator’s Guide- P18:The release of the XNA platform and specifically the ability for anyone to write Xbox 360 console games was truly a major progression in the game-programming world. Before XNA, it was simply too complicated and costly for a student, software hobbyist, or independent game developer to gain access to a decent development kit for a major console platform.
AMBIENT/
Chủ đề:
Bình luận(0) Đăng nhập để gửi bình luận!
Nội dung Text: Microsoft XNA Game Studio Creator’s Guide- P18
- 488 MICROSOFT XNA GAME STUDIO CREATOR’S GUIDE in a SoundEffect object. A SoundEffectInstance is created from this SoundEffect ob- ject which gives you the ability to loop, pause, and resume your individual sound ef- fects. The firing audio is loaded into a SoundEffect object which is used later for playback whenever the B button is pressed. // load and play background audio backgroundAudio = Content.Load("Audio\\telemetricBeep"); MediaPlayer.Play(backgroundAudio); MediaPlayer.IsRepeating = true; // load and play engine as SoundEffectInstance for playback control engineSound = Content.Load("Audio\\engine0"); engineInstance = engineSound.Play(VOLUME, PITCH, PAN, LOOP); // load fire sound effect for playback later fireSound = Content.Load("Audio\\fire"); Lastly, inside Update(), you can add this code to trigger the firing audio whenever the B button is pressed. This code will also allow your players to pause and resume the ship engine and telemetric beep whenever they press the center of the Zune pad. // get new game pad states GamePadState gpCurrent = GamePad.GetState(PlayerIndex.One); // play fire sound at each new B button press event if (gpCurrent.IsButtonDown(Buttons.B)&& gpPrevious.IsButtonUp(Buttons.B)) fireSound.Play(VOLUME, PITCH, PAN, !LOOP); // toggle engine audio pause and resume states for DPad.Down events if (gpCurrent.Buttons.A == ButtonState.Pressed && gpPrevious.Buttons.A == ButtonState.Released) if (engineInstance.State == SoundState.Playing){ engineInstance.Pause(); MediaPlayer.Pause(); } else{ engineInstance.Resume(); MediaPlayer.Resume(); } // store game pad state for next frame gpPrevious = gpCurrent; You are now ready to deploy this to the Zune for an audio-enabled experience. You may have found the process so simple that you might also decide to use this li- brary in your 2D and 3D game projects on the PC and Xbox 360 as well.
- C H A P T E R 2 7 489 Adding Audio to Your Game We are sure you will notice how much more enjoyable your games are when you add audio using any of the alternatives provided in XNA. C HAPTER 27 REVIEW EXERCISES To get the most from this chapter, try out these chapter review exercises. 1. Implement the step-by-step example in this chapter to create your own XACT audio project file. Then load your audio and play it from code. 2. Using the solution for the arcing projectiles example from Chapter 19, add in audio to handle a launch sound and a 3D audio-enabled explosion sound when the rocket hits the ground. 3. Using the solution for “Adding a Car as a Third-Person Object” from Chapter 14, add a looping noise that repeats to create a continuous engine sound whenever the car moves forward or backward.
- This page intentionally left blank
- CHAPTER 28 Multiplayer Gaming
- sure you can appreciate the difference between playing WE’RE video games against the computer and against your friends. Whether you’re knocking baseballs over their heads, swerving in front of them to maintain a lead, or volleying rockets at them, it’s all good. Most people have a lot more fun playing against an unpredictable human opponent who puts up a tough fight and trash-talks while doing it. Until now, the examples in this book have been geared for single-player games. You can easily change this by converting your base code to enable a multiplayer envi- ronment—where up to four people at a time can take the controls in a split-screen game. This type of environment is exactly what you would expect in a 3D first-per- son shooter game or a racing game. You could actually split the screen into more than four sections, but the controller limit is four. You might want additional dimensions, though, if you were to show dif- ferent views of the world. For example, maybe you want to create a radar screen with an aerial view of your entire world in addition to the main viewer for navigation. The split-screen technique offers many useful possibilities for dividing up the graphics that are rendered in your window. The code changes required to enable a split-screen game are surprisingly simple. The Viewport class makes it easy to split your screen. And if your camera is care- fully designed, as it is in the examples this book, you can easily create separate in- stances to give each additional player control over her own viewport. V IEWPORT A viewport is a section of the window that you use to draw a scene from a specific view. As you’ll see, using the Viewport class to split the screen is actually very sim- ple. The Viewport class is used to create a viewport object: Viewport viewport = new Viewport(); Each viewport object has several properties to set the position and area covered in the game window. Each section of the window is assigned values for the starting top-left pixel, the width and height in pixels, and the depth for clipping, so your phys- ical game objects draw properly: int viewport.X // top left pixel X coordinate int viewport.Y // top left pixel Y coordinate int viewport.Width // width in pixels int viewport.Height // height in pixels float viewport.MinDepth // minimum depth of clip volume (usually 0) float viewport.MaxDepth // maximum depth of clip volume (usually 1) 492
- C H A P T E R 2 8 493 Multiplayer Gaming The bulk of the code changes needed to convert to a multiplayer game are in han- dling a separate instance of the camera for each player. However, even this task is rel- atively simple. When your multiplayer games are rendered on the Xbox 360, your viewports may be truncated on the televisions where they are played. In fact, it is possible that up to 20 percent of the screen will be hidden. This issue can be addressed by implementing a routine to create margins that equal 10 percent of the window height at the top and bottom and 10 percent of the window width for the left and right. An example of how to do this is presented in the demonstration later in this chapter. C REATING SEPARATE CAMERAS FOR EACH PLAYER To give each user the ability to navigate through the world, a separate camera in- stance is required for each player. The camera instance gives the players the ability to change their position and view within the 3D world. Adjusting the View For the graphics engine used in this book, whenever a player moves the mouse or shifts the right thumbstick, he changes his view. In other words, his position in the world stays the same, but his Look direction changes as his view target changes. A separate view is needed for each player in the game. For example, in a racing game you might need to focus your camera to watch the contours of a hairpin turn so you don’t crash. Your friend might need to watch out for an oil slick to maintain control of the car, and yet another player might be focused on the finish line. When you assign a separate viewport for each player, every object that is drawn in the viewport must be rendered according to that player’s view. Even the base code, which draws nothing but ground, must draw the ground once for each viewport ac- cording to the viewport owner’s Look direction. To handle this need for separate views, the camera’s view matrix is updated separately for each player. Adjusting the Projection The Projection matrix transforms vector coordinates into clip space (a cone that the viewer sees through). In a split-screen window, you must also adjust the projection to match the area in each viewport. If you do not resize the perspective’s aspect ratio properly, you could end up with a viewport(s) that displays everything in a bloated manner—as if the scene were either viewed through a fish-eye lens or in a house of mirrors. The aspect ratio considers the viewport width relative to the viewport height. Until now we have used the window width over height to calculate this ratio. Now though, the current viewport’s width over height is needed.
- 494 MICROSOFT XNA GAME STUDIO CREATOR’S GUIDE To implement this modification for a split-screen environment, for each different viewport size on display, a Projection matrix is defined when the application begins with the following syntax: // parameters are field of view, viewport w/h, near clip, far clip Matrix projection = Matrix.CreatePerspectiveFieldOfView( float fieldOfView, float aspectRatio, float nearClip, float farClip) If you divide the window into top and bottom viewports, the aspect ratio be- comes this: Window.ClientBounds.Width/(Window.ClientBounds.Height/2) H ANDLING THE USER INPUT It is possible to have up to four game controllers on one Xbox 360 or PC, so you could write code to handle up to four different players and split the screen accord- ingly at run time. For the PC, you can even use the mouse and keyboard as one of these inputs. Handling the different controllers is easy with the GamePadState class because each controller is referenced by a separate instance of the class. The states for each control on the game pad can be obtained using the GetState() method with a PlayerIndex attribute as a parameter to specify each player. S PLIT-SCREEN CODE EXAMPLE This example demonstrates multiplayer 3D gaming in a split-screen environment. Two aliens will be rendered and controlled in two separate viewports. Each player has her own viewport and is given control of one alien’s spaceship, which moves with her camera as she travels. Figure 28-1 shows a split screen for two players. Each player can control her view and position inside the viewport and ultimately travel within the world independently of the other player. A multiplayer racing game or first-person shooter game uses the same code founda- tion, so converting the logic to suit a different type of 3D multiplayer game is a simple task. Converting this logic to handle more than two players is also straightforward. When you run this code on the Xbox 360, you will be able to handle two players, each with her own controller. When the code is run on the PC, you can handle either two controllers, or one controller and a mouse/keyboard combination. If you run this code on the PC with only a mouse and keyboard, you will be able to control one of the viewports, but the other viewport will be disabled until a controller is con- nected.
- C H A P T E R 2 8 495 Multiplayer Gaming This example begins with either the MGHWinBaseCode or MGH360BaseCode project, which can be found in the BaseCode folder in the download available from this book’s website. To enable a two-player game, and to identify each player, you declare the NUMPLAYERS, ALIEN0, and ALIEN1 definitions at the top of the game class: const int NUMPLAYERS = 2; const int ALIEN0 = 0; const int ALIEN1 = 1; To give each player control to move through the 3D environment, and to allow them to view it independently, you declare an array with two separate instances for the camera. Use this revision to replace the existing camera object declaration: private Camera[] cam = new Camera[NUMPLAYERS]; FIGURE 28-1 Two viewports for a two-player game. Each player controls her view of the world and can travel independently.
- 496 MICROSOFT XNA GAME STUDIO CREATOR’S GUIDE When you’re initializing each camera, the starting position and view position for each person needs to be different. Otherwise, with just a default position and view, when the game begins, the players would all be positioned in the same place, one on top of the other. To set the players up at opposite ends of the world, and to have them looking at their opponent, each instance of the camera is initialized with parameters to set the position and view. An override to the camera constructor allows you to set the position and view of the camera when it is initialized for each player: public Camera(Vector3 startPosition, Vector3 startView){ position = startPosition; view = startView; up = new Vector3(0.0f, 1.0f, 0.0f); } To initialize the camera for the players, you pass their individual starting positions and views to the camera constructor. This is done from the Initialize() method (in the game class) at the program start: Vector3 position, view; position = new Vector3( 0.5f, 0.9f, BOUNDARY - 0.5f); view = new Vector3( 0.5f, 0.7f, BOUNDARY - 1.0f); cam[0] = new Camera(position, view); position = new Vector3(-0.5f, 0.9f,-BOUNDARY + 0.5f); view = new Vector3(-0.5f, 0.7f,-BOUNDARY + 1.0f); cam[1] = new Camera(position, view); As mentioned earlier in this chapter, because both viewport heights are half the ac- tual window height, the aspect-ratio parameter in the Projection matrix must be ad- justed. The aspect ratio for the projection becomes (width/(height/2)). To apply this to the Projection matrix, after initializing each camera, replace the call to initialize the Projection matrix in the Initialize() method: for (int player = 0; player < 2; player++) cam[player].SetProjection(Window.ClientBounds.Width, Window.ClientBounds.Height/2); Now that you have properly set up the projection matrix for a multiplayer envi- ronment, you will need to comment out the original call statement to initialize the p ro jec tio n matr ix. You can f i n d t h i s cal l st a t em en t i n si d e t h e InitializeBaseCode() method of the game class:
- C H A P T E R 2 8 497 Multiplayer Gaming // cam.setProjection(Window.ClientBounds.Width, // Window.ClientBounds.Height); A routine is needed in the game class to determine how many game controllers are connected so it can assign control to both players accordingly. The TotalControllersUsed() method needed for this example only considers a sit- uation where up to two controllers are used: int TotalControllersUsed(){ GamePadState gp0 = GamePad.GetState(PlayerIndex.One); GamePadState gp1 = GamePad.GetState(PlayerIndex.Two); if (gp0.IsConnected && gp1.IsConnected) return 2; else if (gp0.IsConnected) return 1; return 0; } The ChangeView(), Move(), Strafe(), and DrawGround() methods inside the game class need to be modified so they can be used for each player. Since each player is a viewport owner, these method headers must be adjusted to accept the player number, as follows: Vector2 ChangeView(GameTime gameTime, int player) float Move(int player) float Strafe(int player) void DrawGround(int player) The ChangeView(), Move(), and Strafe() methods are called once for each viewport owner. These methods must then select the correct input device to allow each player to control their view and position. For this example, by default, a GamePadState object is set for the first player us- ing the PlayerIndex.One parameter value regardless of whether a controller is connected or not. If zero or one controllers are connected on a PC, the mouse is desig- nated for the first player to control their viewport. If two game controllers are con- nected on the PC, the GamePadState object is set for the second player using the PlayerIndex.Two parameter. The following code must be added in the ChangeView(), Move(), and Strafe() methods after the GamePadState object, gp, is declared: bool useMouse = false; int totalControllers = TotalControllersUsed();
- 498 MICROSOFT XNA GAME STUDIO CREATOR’S GUIDE // when fewer than two controllers connected use mouse for 1st player if (totalControllers
- C H A P T E R 2 8 499 Multiplayer Gaming televisions, this nonvisible range may be as high as 20 percent. To adjust for this pos- sibility, the starting top-left pixel that is used as the viewport should allow for this po- tential difference. When viewports are used, you are going to want to account for this possibility so that your 3D graphics do not appear to be off center if truncation oc- curs. To fix this, add this version of the TitleSafeRegion() method to obtain the bounding margins for the top viewport. These margins in turn will be used to deter- mine the starting top-left pixel for each viewport in this demonstration: Rectangle TitleSafeRegion(){ int windowWidth = Window.ClientBounds.Width; int windowHeight = Window.ClientBounds.Height; #if Xbox // some televisions only show 80% of the window Vector2 start = new Vector2(); // starting pixel X & Y const float UNSAFEAREA = 0.2f; // 80% not visible on // Xbox 360 start.X = windowWidth * UNSAFEAREA/2.0f; start.Y = windowHeight * UNSAFEAREA/2.0f; // ensure viewport drawn in safe region on all sides return new Rectangle( (int)start.X, (int)start.Y, (int)((1.0f-UNSAFEAREA)* windowWidth), (int)((1.0f-UNSAFEAREA)* windowHeight/2)); #endif // PC show the entire region return new Rectangle(0, 0, windowWidth, windowHeight/2); } The next method needed in your game class is the CurrentViewport() method to set your viewport. In a multiplayer game, the Draw() method must trigger rendering of the entire scene for each viewport. Before drawing each viewport, you must set the top-left pixel where the viewport begins, the height and width properties for each viewport, and the clip minimum and maximum. If the clip minimum and maximum values are not set between 0 and 1, your 3D models will not render properly: Viewport CurrentViewport(int player){ Viewport viewport = new Viewport(); Rectangle safeRegion = TitleSafeRegion(); Vector2 startPixel = new Vector2((float)safeRegion.Left, (float)safeRegion.Top);
- 500 MICROSOFT XNA GAME STUDIO CREATOR’S GUIDE // get starting top left pixel for viewport if (player == 1) // 2nd player - bottom startPixel.Y += (float)safeRegion.Height; // assign viewport properties viewport.X = (int)startPixel.X; // top left pixel X viewport.Y = (int)startPixel.Y; // top left pixel Y viewport.Width = safeRegion.Width; // pixel width viewport.Height = safeRegion.Height; // pixel height viewport.MinDepth = 0.0f; // depth is between viewport.MaxDepth = 1.0f; // 0 & 1 so models // appear properly return viewport; } When multiple viewports are used, each one is rendered separately. In effect, the same scene is drawn more than once. With careful planning, the same methods can be used for drawing your primitive objects and models. Note also that when drawing with multiple viewports, the viewport is set first before the viewport is cleared. The drawing is performed afterward. Replace the existing Draw() method with this revision to draw all objects in each viewport according to the view and perspective of each player: protected override void Draw(GameTime gameTime){ for (int player = 0; player < NUMPLAYERS; player++) { // set the viewport before clearing screen graphics.GraphicsDevice.Viewport = CurrentViewport(player); graphics.GraphicsDevice.Clear(Color.CornflowerBlue); // draw objects DrawGround(player); } base.Draw(gameTime); } If you ran the project now, you would see two viewports. Remember that this code can serve as a base for any multiplayer game. To make this demonstration more interesting, two aliens will be added. Each player will be given control of an alien, which will be used as a third-person charac- ter. The alien will move with the camera. This not only allows each player to control her own spaceship, but also enables her to view the movements of her opponent in her own viewport.
- C H A P T E R 2 8 501 Multiplayer Gaming For this example, you can use the alien models in the Models folder in the book’s download. To do this, obtain the alien0.fbx, alien1.fbx, and spaceA.bmp files from the Models folder. Create a Models folder in your project and reference the .fbx files from the Solution Explorer. To load these models and to control their transformations, declarations for the model objects and their bone-transformation matrices are required at the top of the game class: Model alien0Model; Model alien1Model; Matrix[] alien0Matrix; Matrix[] alien1Matrix; The code used to load these two models and their accompanying transformation matrices is contained in the InitializeAliens() method. To initialize the models, add this method to your game class: void InitializeAliens(){ alien0Model = Content.Load("Models\\alien0"); alien0Matrix = new Matrix[alien0Model.Bones.Count]; alien0Model.CopyAbsoluteBoneTransformsTo(alien0Matrix); alien1Model = Content.Load("Models\\alien1"); alien1Matrix = new Matrix[alien1Model.Bones.Count]; alien1Model.CopyAbsoluteBoneTransformsTo(alien1Matrix); } To load the aliens when the program begins, add the call statement InitializeAliens() to the Initialize() method: InitializeAliens(); For this two-player game, one alien and its spaceship are controlled by each player. Each alien’s spaceship moves with the player’s camera. To rotate the alien about the Y axis—so it always points in the direction it is traveling—use the follow- ing method to calculate the angle of direction based on the camera’s Look direction: float RotationAngle(Vector3 view, Vector3 position){ Vector3 look = view - position; return (float)Math.Atan2((double)look.X, (double)look.Z); } To save on code, the same method is used to draw both aliens. When these items are rendered in a viewport, this method is called once for each model. This process is
- 502 MICROSOFT XNA GAME STUDIO CREATOR’S GUIDE repeated for each player. For this example, alien0’s position and angle of orienta- tion is based on the first player’s camera. Alien1’s position and orientation is based on the second player’s camera. The view is adjusted for each player. The rest of the routine is identical to the routines you have already used in this book for drawing models. void DrawAliens(int player, Model model, int modelNum){ foreach (ModelMesh mesh in model.Meshes){ // 1: declare matrices Matrix world, scale, rotationY, translation, translationOrbit; // 2: initialize matrices scale = Matrix.CreateScale(0.5f, 0.5f, 0.5f); translation = Matrix.CreateTranslation(Vector3.Zero); rotationY = Matrix.CreateRotationY(MathHelper.Pi); translationOrbit = Matrix.CreateTranslation(0.0f, 0.0f, 1.0f); translation = Matrix.CreateTranslation( // one alien cam[modelNum].position.X, // is located cam[modelNum].position.Y - 0.6f, // at each cam[modelNum].position.Z); // camera float angleY = RotationAngle(cam[modelNum].view, cam[modelNum].position); rotationY = Matrix.CreateRotationY(angleY); // 3: build cumulative world matrix using I.S.R.O.T. sequence // identity, scale, rotate, orbit(translate & rotate), translate world = scale * translationOrbit * rotationY * translation; foreach (BasicEffect effect in mesh.Effects){ // 4: pass wvp to shader effect.View = cam[player].viewMatrix; switch (modelNum){ case ALIEN0: effect.World = alien0Matrix[mesh.ParentBone.Index] * world; break; case ALIEN1: effect.World = alien1Matrix[mesh.ParentBone.Index] * world; break; } effect.Projection = cam[player].projectionMatrix; // 4b: set lighting effect.EnableDefaultLighting();
- C H A P T E R 2 8 503 Multiplayer Gaming } // 5: draw object mesh.Draw(); } } To draw each model, add these call statements to the end of the Draw() method inside the for-loop that triggers drawing for each player’s viewport: DrawAliens(player, alien0Model, ALIEN0); DrawAliens(player, alien1Model, ALIEN1); When you run this version of the code, each of the two players can control the movement of her alien separately. In addition, she can view her world and travel in- dependently of the other player. Being able to shift the view up and down might be useful for a first-person shooter game, but it doesn’t look right for this setup. Inside ChangeView(), you can pre- vent the camera from bobbing up and down by modifying the return statement to set the Y view to zero: return new Vector2(change.X, 0.0f); When you run the code now, each player will be able to control her view of the world. This example was kept simple, but you can apply this logic for different types of games, such as first-person shooter, racing, role-playing, or adventure games. C HAPTER 28 REVIEW EXERCISES To get the most from this chapter, try out these chapter review exercises. 1. If you have not already done so, implement the step-by-step demonstration presented in this chapter. 2. Create a three-person viewport window. The first two viewports should split the top half of the window, and the third viewport should be located in the bottom half of the window. The user input should be handled in such a way as to permit up to three controllers. If the code is run on the PC, and fewer than three controllers are detected, the mouse and keyboard should be enabled for the third player. You will need to create a separate Projection matrix method to permit proper viewing from each viewport if one of the viewports is sized differently than the others.
- This page intentionally left blank
- CHAPTER 29 Networking
- to Microsoft XNA’s developer-friendly frame- THANKS work, building network games is simple and pow- erful. The examples presented in this chapter use 3D graphics, so they will only work on the PC and Xbox 360. However, the network-specific code is identical for the Xbox 360, PC, and Zune. If needed, you could easily adapt these solutions for game play in a 2D environment to run on the Xbox 360, PC, or Zune. This chapter offers two different ways to implement the XNA networking frame- work through: peer-to-peer network and a client/server network. P EER-TO-PEER NETWORKS The simpler option for developing XNA network games is to use a peer-to-peer network. The peer-to-peer network is generated by the first machine to create a game session. Once the session is started, the other peer machines may join. Each peer system reads data sent from all other peers, so the traffic load is equivalent for all of the machines us- ing the network. When all peer machines have similar processing capabilities, the peer-to-peer network may be ideal for enabling efficient and balanced game perfor- mance. However, a peer-to-peer framework creates more network traffic than a cli- ent/server structure. As more gamers join the peer-to-peer network session, performance will generally degrade more quickly than on a client/server-based network. C LIENT/SERVER NETWORKS On a client/server network all remote machines, referred to as clients, send their game data to the server, and the server distributes the entire data collection to all cli- ents. The machine that creates the session is designated as the server. A client/server network offers better scalability for games because it cuts down on data packet traffic and it lets gamers decide where the server-side processing should be performed. Having the data managed in one location also enables better security enforcement. E FFICIENT BANDWIDTH USE Exchanging data over a network presents challenges since there are delays between the time data packets are sent and when they are received. More data and more gamers will slow the network traffic. Because of this, ensuring efficient data delivery speed is crucial not only for game performance but also for game play. If two cars are in a dead heat and each car is controlled from a remote machine, you need to be sure to reward the true winner. In a networked environment this can be difficult. Data de- livery may fall behind the game action when two cars are jockeying for position. In 506 close calls like these, it is hard to know who really won. You could just assign a tie if
- C H A P T E R 2 9 507 Networking you are unsure who won, but your players will eventually catch on and get upset if tied results occur too frequently. Also, if the network is too slow, your animations will lag and could appear fragmented. To ensure efficient use of your network, you should aim to minimize the amount of data exchanged. For this reason, your game design should arrange your data pack- ets such that only critical data has to be sent. During a grenade explosion, random smoke, flames, shrapnel, and collateral damage result. Given all of the events and as- sets affected by an explosion, you cannot afford to transfer all of this descriptive data over the network to create an identical explosion on each client. To reduce traffic, much of the randomization should be done on the client—this is how commercial games do it. If you actually compare the aftermath of an explosion on two different machines in a networked game, you will see differences in how the windows, build- ings, and vehicles are damaged and dispersed. Still, when an explosion takes place, players on different machines are seeing this action from different directions. Game play in games like these is usually so fast that minor differences aren’t noticeable and they certainly do not detract from the experience. X NA’S CODE FRAMEWORK XNA offers a very simple interface to build your network while it also allows you room to customize how it operates. GamerServicesComponent A networked XNA game is enabled with the GamerServicesComponent. The GamerServices namespace must be referenced from the game class to use it: using Microsoft.Xna.Framework.GamerServices; The GamerServicesComponent is added when the game begins: Components.Add(new GamerServicesComponent(this)); NetworkSession A multiplayer network game is conducted through one network session. Each system that joins the networked game contains the same instance of this session. NetworkSession session; Creating the Network The first machine must initialize the NetworkSession object using the NetworkSession class Create() method. Create() receives three parameters.
ADSENSE
CÓ THỂ BẠN MUỐN DOWNLOAD
Thêm tài liệu vào bộ sưu tập có sẵn:
Báo xấu
LAVA
AANETWORK
TRỢ GIÚP
HỖ TRỢ KHÁCH HÀNG
Chịu trách nhiệm nội dung:
Nguyễn Công Hà - Giám đốc Công ty TNHH TÀI LIỆU TRỰC TUYẾN VI NA
LIÊN HỆ
Địa chỉ: P402, 54A Nơ Trang Long, Phường 14, Q.Bình Thạnh, TP.HCM
Hotline: 093 303 0098
Email: support@tailieu.vn