Существуют две основные модели взаимодействия сетевых игр.
Первая модель – это Peer-to-peer – то есть, архитектура, основанная на взаимодействии равноправных программ. Такая модель взаимодействия предусматривает равноправие каждой из запущенных копий игры. При подобном способе взаимодействия каждая из запущенных копий игры равноправна, она обязана отслеживать состояние других копий, отправлять каждой из них сообщения. Такая модель взаимодействия подходит лишь для игр, предусматривающих небольшое количество игроков – при росте количества лавинообразно увеличивается поток сообщений, которыми они вынуждены обмениваться для поддержания игрового процесса. Фактически, каждая из копий игры связана с каждой из других копий. Схематически модель взаимодействия peer-to-peer вы можете видеть на рис. 16.1.
(рис 16.1) Модель взаимодействия Peer-to-PeerКак правило, в такой модели взаимодействия одна из игр назначается хостом – именно к ней должны обратиться новые игроки для того, чтобы принять участие в игре. Однако хост не управляет обменом сообщениями между играми
Вторая модель сетевого взаимодействия игр – это модель Client/Server (Клиент/Cервер). При таком взаимодействии выделяется два вида программ. Первая – это программа-сервер, на которую ложатся все заботы по организации игры и по взаимодействию с программами-клиентами. При таком подходе клиенты обмениваются сообщениями лишь с сервером, не взаимодействия друг с другом напрямую. Этот подход позволяет создавать игры, эффективно использующие пропускную способность каналов связи и поддерживающие одновременное участие в игре множества игроков. На рис. 16.2. вы можете видеть схематически изображенную модель взаимодействия Client/Server.
(рис 16.2) Модель взаимодействия Client/ServerКаждая из игр одноранговой архитектуры является самостоятельной единицей, содержащей в себе все необходимое для полноценной организации игрового процесса. Фактически, каждая из копий игры работает самостоятельно, отсылая другим копиям информацию о своем состоянии и получая от других игр данные об их состоянии. При клиент-серверной схеме взаимодействия возможны несколько подходов. При первом каждый клиент является полноценной программой, которая выполняет все необходимые вычисления и отсылает серверу лишь их результаты. В итоге сервер превращается в инструмент для рассылки информации о состоянии игр между подключенными к нему клиентами. Второй подход заключается в большей нагруженности сервера – так, например, все игровые вычисления могут вестись на сервере, а клиенты используются лишь для приема ввода пользователя и отправки его на сервер, а так же для визуализации принятых от сервера данных.
Возможны и другие модели организации сетевого взаимодействия игр. Например – кольцевая система обмена сообщениями или модель, предусматривающая наличие нескольких игровых серверов.
Сетевые игры можно подразделить на два вида. Первый вид – это пошаговые игры (Turn Based Games). Яркий пример подобных игр – шахматы, шашки, компьютерные представления настольных игр. Игрок имеет определенное время на то, чтобы принять решение об очередном действии, ходе, а другие игроки вынуждены ждать своей очереди. Такие игры хорошо переносят проблемы сетевой связи – задержка прибытия сообщения на несколько секунд не слишком испортит впечатление от неторопливого шахматного турнира.
Второй вид игр – это игры реального времени (Real Time Games). К таким играм относится, например, популярная Counter Strike. Действия игроков происходят в реальном времени – даже небольшая проблема со связью способна очень сильно помешать игровому процессу.
Многие современные игры, не поддерживающие игру по сети, предусматривают хранение результатов игр на специальных интернет-сервисах фирм-производителей игр, позволяя игрокам соревноваться друг с другом "заочно".
XNA поддерживает различные способы организации сетевого взаимодействия игр.
Мы отметим здесь два основных вида сетевого взаимодействия игр, которые поддерживает XNA.
Первый вид – это игры, которые используют для взаимодействия сервис Microsoft LIVE. Например – одна из моделей взаимодействия через LIVE организуется с помощью сессии типа NetworkSessionType.PlayerMatch. Для того, чтобы играть в такие игры, нужно подключение к Интернету, нужно получить учетную запись на сервере LIVE. Этот вид сетевых игр позволяет объединять игроков без учета их территориальной расположенности или других подобных ограничений.
Второй вид – это игры, которые могут взаимодействовать лишь в локальной сети. Эта модель взаимодействия реализуется с помощью сетевой сессии типа NetworkSessionType.SystemLink. Для организации игры не нужно подключение к Интернету, LIVE-аккаунт. Эта модель взаимодействия хорошо подходит для простых игр, однако, если вы хотите серьезно заниматься разработкой сетевых игр на платформе XNA – ориентируйтесь на LIVE-игры.
Рассмотрим последовательность создания сетевой составляющей компьютерной игры.
Для того чтобы создать сетевую игру, нужно выполнить следующие шаги:
Рассмотрим создание сетевой SystemLink-игры, которая может работать в локальной сети.
Разработаем сетевую версию клона популярной игры Pong. Реализуем в нашем примере базовую функциональность сетевой игры, которая, при практическом применении, должна быть дополнена.
Создадим новый игровой проект P12_1. На рис. 16.3. вы можете видеть окно Solution Explorer для этого проекта.
(рис 16.3) Окно Solution Explorer для проекта P12_1Здесь мы использовали уже знакомый вам набор текстур BallandBats, содержащий текстуры для клона игры в Pong. Основная функциональность сосредоточена в классе Game1. Классы Ball и Bat используются для представления соответствующих объектов в игре.
Алгоритм работы с программой выглядит так. Сразу после запуска она запускает интерфейс для создания учетной записи игрока, после того, как игрок войдет в систему, запускается игра. После запуска программа пытается автоматически найти сетевую сессию, созданную другой аналогичной программой. Если попытка поиска не увенчалась успехом, программа самостоятельно создает сессию и ждет подключения другой программы. После подключения начинается игра.
В листинге 16.1. вы можете найти код класса Ball. Этот класс используется для хранения атрибутов мяча, а так же – содержит код для вывода объекта на экран.
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
namespace P12_1
{
//Класс мяча
//Обычный класс, содержащий данные о мяче и процедуру визуализации
class Ball
{
//Текстура
Texture2D sprTexture;
//Прямоугольник, ограничивающий нужную текстуру
public Rectangle sprRectangle;
//Позиция на экране
public Vector2 sprPosition;
//Скорость перемещения
public Vector2 sprSpeed;
//Границы экрана
public Rectangle scrBounds;
//Конструктор - принимает на вход объект для загрузки
//контента и ссылку на игру
public Ball(ContentManager content, Game game)
{
//Загрузим текстуру
sprTexture = content.Load<Texture2D>("Ballandbats");
//Установим прямоугольник
sprRectangle = new Rectangle(16, 203, 17, 17);
//Установим скорость
sprSpeed = new Vector2(-1.5f, -2.7f);
//Вычислим границы экрана
scrBounds = new Rectangle(0, 0,
game.Window.ClientBounds.Width,
game.Window.ClientBounds.Height);
//Установим мяч в центр экрана
sprPosition = new Vector2(scrBounds.Width / 2, scrBounds.Height / 2);
}
//Процедура для рисования объекта
//Принимает на вход объект SpriteBatch
//Используется в основной игре внутри команд Begin и End
//в цикле вывода изображений с помощью объекта типа SpriteBatch
public void Draw(SpriteBatch spriteBatch)
{
spriteBatch.Draw(sprTexture, sprPosition, sprRectangle, Color.White);
}
}
}
В листинге 16.2. вы можете найти код класса Bat. Он почти так же прост, как и код мяча. Единственное существенное отличие – он включает в себя проверку на столкновение с верхней и нижней границами экрана. Эта проверка происходит каждый раз после вызова процедуры вывода объекта на экран.
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
namespace P12_1
{
//Класс биты
class Bat
{
//Текстура
Texture2D sprTexture;
//Прямоугольник, ограничивающий нужную текстуру
public Rectangle sprRectangle;
//Позиция
public Vector2 sprPosition;
//Границы экрана
Rectangle scrBounds;
//Конструктор - принимает на вход номер игрока, объект для загрузки
//контента и ссылку на игру
public Bat(int PlIndex, ContentManager content, Game game)
{
//Загрузим текстуру
sprTexture = content.Load<Texture2D>("Ballandbats");
//Если номер игрока равен нулю (то есть - это хост)
if (PlIndex == 0)
{
//Установим биту слева
sprRectangle = new Rectangle(18, 9, 17, 88);
sprPosition = new Vector2(0, 0);
}
//Если номер равен 1 (это - обычный игрок)
if (PlIndex == 1)
{
//Установим биту справа
sprRectangle = new Rectangle(17, 106, 17, 88);
sprPosition = new Vector2(game.Window.ClientBounds.Width-sprRectangle .Width, 0);
}
//Границы экрана
scrBounds = new Rectangle(0, 0,
game.Window.ClientBounds.Width,
game.Window.ClientBounds.Height);
}
//Процедура визуализации, которую вызываем из основной программы
public void Draw(SpriteBatch spriteBatch)
{
//Перед визуализацие проверяем, не вышел ли объект за пределы экрана
//Если вышел - исправляем
if (sprPosition.Y < scrBounds.Y) sprPosition.Y = scrBounds.Y;
if (sprPosition.Y + sprRectangle.Height > scrBounds.Height) sprPosition.Y = scrBounds.Height - sprRectangle.Height;
//выводим объект на экран
spriteBatch.Draw(sprTexture, sprPosition, sprRectangle, Color.White);
}
}
}
Теперь рассмотрим код главного класса нашей игры. В листинге 16.3. вы можете найти код класса Game1. Именно он реализует сетевую функциональность для нашей игры. При разработке этого класса мы пользовались справочными материалами Microsoft.
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
namespace P12_1
{
public class Game1 : Microsoft.Xna.Framework.Game
{
//Для управления графическим устройством
GraphicsDeviceManager graphics;
//Для вывода изображений
SpriteBatch spriteBatch;
//Для объекта-мяча
Ball b;
//Для обработки состояния клавиатуры
KeyboardState Kb;
//Для сетевой сессии
NetworkSession networkSession;
//Для записи и чтения пакетов данных
PacketWriter packetWriter;
PacketReader packetReader;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
//Добавляем новый GamerServiceComponent
Components.Add(new GamerServicesComponent(this));
}
protected override void Initialize()
{
//Объекты для записи и чтения данных созданы
//с конструкторами по умолчанию
packetWriter = new PacketWriter();
packetReader = new PacketReader();
base.Initialize();
}
/// <summary>
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
//Создадим новый мяч
b = new Ball(Content, this);
}
protected override void UnloadContent()
{
// TODO: Unload any non ContentManager content here
}
protected override void Update(GameTime gameTime)
{
//Состояние клавиатуры
Kb = Keyboard.GetState();
//Если сетевая сессия не создана
if (networkSession == null)
{
//Если окно приложения активно
if (IsActive)
{
//Если ни один игрок не вошел в систему
if (Gamer.SignedInGamers.Count == 0)
{
//Если окно регистрации не видно - отобразить его
if (!Guide.IsVisible)
Guide.ShowSignIn(2, false);
}
else
//Если пользователь вошел в систему
//Запустить процедуру, которая сначала пытается
//найти существующую сетевую сессию
//если не находит - создает новую
TryToJoinOrCreate();
}
}
else
//Если сетевая сессия создана
//Перейти к процедуре обработки сессии
WorkWithSession();
base.Update(gameTime);
}
//Процедура, которая пытается подключиться к существующей сессии
//или создать новую
void TryToJoinOrCreate()
{
//Выводим сообщение в заголовок окна
this.Window.Title = "Ищу доступную сессию";
//Начинаем поиск новой сессии типа SystemLink - такая сессия
//позволяет создавать игры для автономных, не подключенных к Интернету,
//локальных сетей
using (AvailableNetworkSessionCollection availNetSessions =
NetworkSession.Find(NetworkSessionType.SystemLink,
2, null))
{
//Если ни одной сессии нет
if (availNetSessions.Count == 0)
{
this.Window.Title = "Сессия не найдена - создаю новую сессию";
//Создаем новую сессию
networkSession = NetworkSession.Create(NetworkSessionType.SystemLink,
1, 2);
//Подключаем обработчики событий
JoinEventhandlers();
this.Window.Title = "Сессия создана";
}
//Иначе, если сессия найдена
else
{
this.Window.Title = "Сессия найдена, присоединяюсь";
//Присоединяемся к ней и так же подключаем обработчики
networkSession = NetworkSession.Join(availNetSessions[0]);
JoinEventhandlers();
}
}
}
//Подключение обработчиков событий
void JoinEventhandlers()
{
//Объект типа NetworkSession имеет множество событий
//В частности, они используются для того, чтобы показать, что к игре присоединился новый пользователь
//что игра начата, закончен. Здесь мы подключили два события
//Одно из них происходит, когда к игре присоединяется новый игрок, подключим соответствующий обработчик
networkSession.GamerJoined += new EventHandler<GamerJoinedEventArgs>(networkSession_GamerJoined);
//Второе происходит после закрытия сессии
networkSession.SessionEnded += new EventHandler<NetworkSessionEndedEventArgs>(networkSession_SessionEnded);
}
//Обработчик события, происходящего при присоединении нового игрока
void networkSession_GamerJoined(object sender, GamerJoinedEventArgs e)
{
//Получим номер игрока
int plNumber = networkSession.AllGamers.IndexOf(e.Gamer);
//Запишем в поле Tag игрока, которое имеет тип Object, то есть - способно
//принимать любые объекты, новый объект Bat. Номер игрока, который мы передали
//объекту, используется для его установки. В нашей игре могут участвовать лишь два игрока
//поэтому номер 0 используется для установки биты в левой части экрана, а номер 1 -
//в правой. Игрок, создавший сессию, занимает левую часть экрана
e.Gamer.Tag = new Bat(plNumber, Content, this);
}
//Обработчик события, происходящего при закрытии сессии
void networkSession_SessionEnded(object sender, NetworkSessionEndedEventArgs e)
{
//уничтожим сессию
networkSession.Dispose();
networkSession = null;
}
//Процедура обработки событий в течение сессии
void WorkWithSession()
{
//Вызвать процедуру, вычисляющую новое положение игрового объекта
//для локального игрока - в нашем случае это один игрок с номером 0
//и записывающей данные в сетевой поток
CalcAndWrite(networkSession.LocalGamers[0]);
//Обновить состояние сессии
networkSession.Update();
//Если сессия оказалась уничтоженной - выйти из процедуры
if (networkSession == null) return;
//Прочесть сетевые данные и модифицировать состояние объектов
ReadAndImplement(networkSession.LocalGamers[0]);
}
//Вычисления и передача данных в сеть
void CalcAndWrite(LocalNetworkGamer gamer)
{
//Рассматривать объект, записанный в Tag текущего игрока
//как биту - Bat
Bat bat = gamer.Tag as Bat;
//Модифицировать координаты в соответствии с клавиатурными командами
//Клавиша вверх перемещает биту вверх
if (Kb.IsKeyDown(Keys.Up))
{
bat.sprPosition.Y -= 3;
}
//Клавиша вниз - вниз
if (Kb.IsKeyDown(Keys.Down))
{
bat.sprPosition.Y += 3;
}
//Записать положение биты в сеть
packetWriter.Write(bat.sprPosition);
//Если текущий игрок - хост, то есть он создал
//сетевую сессию, на него возложим обязанности
//вычисления позиции мяча и обработки столкновений
if (gamer.IsHost)
{
//Если к игре подключено 2 пользователя
//То есть она началась
if (networkSession.AllGamers.Count == 2)
{
//Изменить позицию мяча в соответствии с его
//скоростью
b.sprPosition += b.sprSpeed;
//Если вышли за пределы верхней части игрового окна
if (b.sprPosition.Y < b.scrBounds.Y)
{
//Вернуть объект и инвертировать скорость по Y
b.sprPosition.Y = b.scrBounds.Y;
b.sprSpeed.Y *= -1;
}
//Если пересекли нижнюю границу окна
//Вернуть объект и инвертировать скорость по Y
if (b.sprPosition.Y + b.sprRectangle.Height > b.scrBounds.Height)
{
b.sprPosition.Y = b.scrBounds.Height - b.sprRectangle.Height;
b.sprSpeed.Y *= -1;
}
//Получить биту для игрока №0 - она расположена слева
//Берем этот объект из списка всех игроков
bat = networkSession.AllGamers[0].Tag as Bat;
//Если есть столкновение с битой
if (b.sprPosition.X + b.sprRectangle.Width > bat.sprPosition.X
b.sprPosition.X < bat.sprPosition.X + bat.sprRectangle.Width
b.sprPosition.Y + b.sprRectangle.Height > bat.sprPosition.Y
b.sprPosition.Y < bat.sprPosition.Y + bat.sprRectangle.Height)
{
//Вернем мяч и инвертируем скорость по X
b.sprPosition.X = bat.sprRectangle.Width;
b.sprSpeed.X *= -1;
}
//Получим биту для игрока №1 - она расположена справа
bat = networkSession.AllGamers[1].Tag as Bat;
//Если есть столкновение с ней
if (b.sprPosition.X + b.sprRectangle.Width > bat.sprPosition.X
b.sprPosition.X < bat.sprPosition.X + bat.sprRectangle.Width
b.sprPosition.Y + b.sprRectangle.Height > bat.sprPosition.Y
b.sprPosition.Y < bat.sprPosition.Y + bat.sprRectangle.Height)
{
//Вернем мяч и инвертируем скорость по X
b.sprPosition.X = b.scrBounds.Width - bat.sprRectangle.Width - b.sprRectangle.Width;
b.sprSpeed.X *= -1;
}
}
//Запишем позицию мяча в сетевой поток
packetWriter.Write(b.sprPosition);
}
//Отправим данные другим игрокам
gamer.SendData(packetWriter, SendDataOptions.InOrder);
}
//Процедура чтения и применения данных от других объектов
void ReadAndImplement(LocalNetworkGamer gamer)
{
//До тех пор, пока есть данные для чтения
while (gamer.IsDataAvailable)
{
//Новый объект sender типа NetworkGamer
//Он бует использован для хранения ссылки на объект
//который отправил данные
NetworkGamer sender;
// Прочесть данные
gamer.ReceiveData(packetReader, out sender);
//Если данные отправлены не локальным игроком - то есть -
//пришли к нам от одного из сетевых игроков (в нашем случае возможно наличие лишь одного
//такого игрока
if (!sender.IsLocal)
{
//Получить объект bat из поля Tag игрока, отправившего данные
Bat bat = sender.Tag as Bat;
//Записать прочитанные данные в качестве позиции этого объекта
bat.sprPosition = packetReader.ReadVector2();
//Если отправитель данных - хост - дополнительно установить
//позицию мяча, прочитав данные из потока
//Таким образом хост всегда пишет в поток два фрагмента данных
//и если другой объект читает данные хоста - он так же
//воспринимает два фрагмента
if (sender.IsHost) b.sprPosition = packetReader.ReadVector2();
}
}
}
protected override void Draw(GameTime gameTime)
{
graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
//Начало вывода спрайтов
spriteBatch.Begin();
//Если создана сетевая сессия и количество игроков равно 2
//То есть - выполнены условия для начала игры
if (networkSession!=null networkSession .AllGamers .Count==2)
{
//Для каждого игрока из всех игроков, имеющих отношение к данной сессии
foreach (NetworkGamer gamer in networkSession.AllGamers)
{
//Получить объект bat для текущего игрока
Bat bat = gamer.Tag as Bat;
//Вывести объект bat
bat.Draw(spriteBatch);
//Вывести мяч
b.Draw(spriteBatch);
}
}
//Завершить вывод объектов
spriteBatch.End();
base.Draw(gameTime);
}
}
}
На рис. 16.4. вы можете видеть экран выбора игрока. Мы зарегистрировались как Player1.
(рис 16.4) Выбор игрокаНа рис. 16.5 вы можете видеть игровой экран после подключения программы к уже созданной сессии.
(рис 16.5) Игровой экран проекта P12_1 Существуют две основные модели взаимодействия сетевых игр.
Первая модель – это Peer-to-peer – то есть, архитектура, основанная на взаимодействии равноправных программ. Такая модель взаимодействия предусматривает равноправие каждой из запущенных копий игры. При подобном способе взаимодействия каждая из запущенных копий игры равноправна, она обязана отслеживать состояние других копий, отправлять каждой из них сообщения. Такая модель взаимодействия подходит лишь для игр, предусматривающих небольшое количество игроков – при росте количества лавинообразно увеличивается поток сообщений, которыми они вынуждены обмениваться для поддержания игрового процесса. Фактически, каждая из копий игры связана с каждой из других копий. Схематически модель взаимодействия peer-to-peer вы можете видеть на рис. 16.1.
(рис 16.1) Модель взаимодействия Peer-to-PeerКак правило, в такой модели взаимодействия одна из игр назначается хостом – именно к ней должны обратиться новые игроки для того, чтобы принять участие в игре. Однако хост не управляет обменом сообщениями между играми
Вторая модель сетевого взаимодействия игр – это модель Client/Server (Клиент/Cервер). При таком взаимодействии выделяется два вида программ. Первая – это программа-сервер, на которую ложатся все заботы по организации игры и по взаимодействию с программами-клиентами. При таком подходе клиенты обмениваются сообщениями лишь с сервером, не взаимодействия друг с другом напрямую. Этот подход позволяет создавать игры, эффективно использующие пропускную способность каналов связи и поддерживающие одновременное участие в игре множества игроков. На рис. 16.2. вы можете видеть схематически изображенную модель взаимодействия Client/Server.
(рис 16.2) Модель взаимодействия Client/ServerКаждая из игр одноранговой архитектуры является самостоятельной единицей, содержащей в себе все необходимое для полноценной организации игрового процесса. Фактически, каждая из копий игры работает самостоятельно, отсылая другим копиям информацию о своем состоянии и получая от других игр данные об их состоянии. При клиент-серверной схеме взаимодействия возможны несколько подходов. При первом каждый клиент является полноценной программой, которая выполняет все необходимые вычисления и отсылает серверу лишь их результаты. В итоге сервер превращается в инструмент для рассылки информации о состоянии игр между подключенными к нему клиентами. Второй подход заключается в большей нагруженности сервера – так, например, все игровые вычисления могут вестись на сервере, а клиенты используются лишь для приема ввода пользователя и отправки его на сервер, а так же для визуализации принятых от сервера данных.
Возможны и другие модели организации сетевого взаимодействия игр. Например – кольцевая система обмена сообщениями или модель, предусматривающая наличие нескольких игровых серверов.
Сетевые игры можно подразделить на два вида. Первый вид – это пошаговые игры (Turn Based Games). Яркий пример подобных игр – шахматы, шашки, компьютерные представления настольных игр. Игрок имеет определенное время на то, чтобы принять решение об очередном действии, ходе, а другие игроки вынуждены ждать своей очереди. Такие игры хорошо переносят проблемы сетевой связи – задержка прибытия сообщения на несколько секунд не слишком испортит впечатление от неторопливого шахматного турнира.
Второй вид игр – это игры реального времени (Real Time Games). К таким играм относится, например, популярная Counter Strike. Действия игроков происходят в реальном времени – даже небольшая проблема со связью способна очень сильно помешать игровому процессу.
Многие современные игры, не поддерживающие игру по сети, предусматривают хранение результатов игр на специальных интернет-сервисах фирм-производителей игр, позволяя игрокам соревноваться друг с другом "заочно".
XNA поддерживает различные способы организации сетевого взаимодействия игр.
Мы отметим здесь два основных вида сетевого взаимодействия игр, которые поддерживает XNA.
Первый вид – это игры, которые используют для взаимодействия сервис Microsoft LIVE. Например – одна из моделей взаимодействия через LIVE организуется с помощью сессии типа NetworkSessionType.PlayerMatch. Для того, чтобы играть в такие игры, нужно подключение к Интернету, нужно получить учетную запись на сервере LIVE. Этот вид сетевых игр позволяет объединять игроков без учета их территориальной расположенности или других подобных ограничений.
Второй вид – это игры, которые могут взаимодействовать лишь в локальной сети. Эта модель взаимодействия реализуется с помощью сетевой сессии типа NetworkSessionType.SystemLink. Для организации игры не нужно подключение к Интернету, LIVE-аккаунт. Эта модель взаимодействия хорошо подходит для простых игр, однако, если вы хотите серьезно заниматься разработкой сетевых игр на платформе XNA – ориентируйтесь на LIVE-игры.
Рассмотрим последовательность создания сетевой составляющей компьютерной игры.
Для того чтобы создать сетевую игру, нужно выполнить следующие шаги:
Рассмотрим создание сетевой SystemLink-игры, которая может работать в локальной сети.
Разработаем сетевую версию клона популярной игры Pong. Реализуем в нашем примере базовую функциональность сетевой игры, которая, при практическом применении, должна быть дополнена.
Создадим новый игровой проект P12_1. На рис. 16.3. вы можете видеть окно Solution Explorer для этого проекта.
(рис 16.3) Окно Solution Explorer для проекта P12_1Здесь мы использовали уже знакомый вам набор текстур BallandBats, содержащий текстуры для клона игры в Pong. Основная функциональность сосредоточена в классе Game1. Классы Ball и Bat используются для представления соответствующих объектов в игре.
Алгоритм работы с программой выглядит так. Сразу после запуска она запускает интерфейс для создания учетной записи игрока, после того, как игрок войдет в систему, запускается игра. После запуска программа пытается автоматически найти сетевую сессию, созданную другой аналогичной программой. Если попытка поиска не увенчалась успехом, программа самостоятельно создает сессию и ждет подключения другой программы. После подключения начинается игра.
В листинге 16.1. вы можете найти код класса Ball. Этот класс используется для хранения атрибутов мяча, а так же – содержит код для вывода объекта на экран.
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
namespace P12_1
{
//Класс мяча
//Обычный класс, содержащий данные о мяче и процедуру визуализации
class Ball
{
//Текстура
Texture2D sprTexture;
//Прямоугольник, ограничивающий нужную текстуру
public Rectangle sprRectangle;
//Позиция на экране
public Vector2 sprPosition;
//Скорость перемещения
public Vector2 sprSpeed;
//Границы экрана
public Rectangle scrBounds;
//Конструктор - принимает на вход объект для загрузки
//контента и ссылку на игру
public Ball(ContentManager content, Game game)
{
//Загрузим текстуру
sprTexture = content.Load<Texture2D>("Ballandbats");
//Установим прямоугольник
sprRectangle = new Rectangle(16, 203, 17, 17);
//Установим скорость
sprSpeed = new Vector2(-1.5f, -2.7f);
//Вычислим границы экрана
scrBounds = new Rectangle(0, 0,
game.Window.ClientBounds.Width,
game.Window.ClientBounds.Height);
//Установим мяч в центр экрана
sprPosition = new Vector2(scrBounds.Width / 2, scrBounds.Height / 2);
}
//Процедура для рисования объекта
//Принимает на вход объект SpriteBatch
//Используется в основной игре внутри команд Begin и End
//в цикле вывода изображений с помощью объекта типа SpriteBatch
public void Draw(SpriteBatch spriteBatch)
{
spriteBatch.Draw(sprTexture, sprPosition, sprRectangle, Color.White);
}
}
}
В листинге 16.2. вы можете найти код класса Bat. Он почти так же прост, как и код мяча. Единственное существенное отличие – он включает в себя проверку на столкновение с верхней и нижней границами экрана. Эта проверка происходит каждый раз после вызова процедуры вывода объекта на экран.
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
namespace P12_1
{
//Класс биты
class Bat
{
//Текстура
Texture2D sprTexture;
//Прямоугольник, ограничивающий нужную текстуру
public Rectangle sprRectangle;
//Позиция
public Vector2 sprPosition;
//Границы экрана
Rectangle scrBounds;
//Конструктор - принимает на вход номер игрока, объект для загрузки
//контента и ссылку на игру
public Bat(int PlIndex, ContentManager content, Game game)
{
//Загрузим текстуру
sprTexture = content.Load<Texture2D>("Ballandbats");
//Если номер игрока равен нулю (то есть - это хост)
if (PlIndex == 0)
{
//Установим биту слева
sprRectangle = new Rectangle(18, 9, 17, 88);
sprPosition = new Vector2(0, 0);
}
//Если номер равен 1 (это - обычный игрок)
if (PlIndex == 1)
{
//Установим биту справа
sprRectangle = new Rectangle(17, 106, 17, 88);
sprPosition = new Vector2(game.Window.ClientBounds.Width-sprRectangle .Width, 0);
}
//Границы экрана
scrBounds = new Rectangle(0, 0,
game.Window.ClientBounds.Width,
game.Window.ClientBounds.Height);
}
//Процедура визуализации, которую вызываем из основной программы
public void Draw(SpriteBatch spriteBatch)
{
//Перед визуализацие проверяем, не вышел ли объект за пределы экрана
//Если вышел - исправляем
if (sprPosition.Y < scrBounds.Y) sprPosition.Y = scrBounds.Y;
if (sprPosition.Y + sprRectangle.Height > scrBounds.Height) sprPosition.Y = scrBounds.Height - sprRectangle.Height;
//выводим объект на экран
spriteBatch.Draw(sprTexture, sprPosition, sprRectangle, Color.White);
}
}
}
Теперь рассмотрим код главного класса нашей игры. В листинге 16.3. вы можете найти код класса Game1. Именно он реализует сетевую функциональность для нашей игры. При разработке этого класса мы пользовались справочными материалами Microsoft.
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
namespace P12_1
{
public class Game1 : Microsoft.Xna.Framework.Game
{
//Для управления графическим устройством
GraphicsDeviceManager graphics;
//Для вывода изображений
SpriteBatch spriteBatch;
//Для объекта-мяча
Ball b;
//Для обработки состояния клавиатуры
KeyboardState Kb;
//Для сетевой сессии
NetworkSession networkSession;
//Для записи и чтения пакетов данных
PacketWriter packetWriter;
PacketReader packetReader;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
//Добавляем новый GamerServiceComponent
Components.Add(new GamerServicesComponent(this));
}
protected override void Initialize()
{
//Объекты для записи и чтения данных созданы
//с конструкторами по умолчанию
packetWriter = new PacketWriter();
packetReader = new PacketReader();
base.Initialize();
}
/// <summary>
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
//Создадим новый мяч
b = new Ball(Content, this);
}
protected override void UnloadContent()
{
// TODO: Unload any non ContentManager content here
}
protected override void Update(GameTime gameTime)
{
//Состояние клавиатуры
Kb = Keyboard.GetState();
//Если сетевая сессия не создана
if (networkSession == null)
{
//Если окно приложения активно
if (IsActive)
{
//Если ни один игрок не вошел в систему
if (Gamer.SignedInGamers.Count == 0)
{
//Если окно регистрации не видно - отобразить его
if (!Guide.IsVisible)
Guide.ShowSignIn(2, false);
}
else
//Если пользователь вошел в систему
//Запустить процедуру, которая сначала пытается
//найти существующую сетевую сессию
//если не находит - создает новую
TryToJoinOrCreate();
}
}
else
//Если сетевая сессия создана
//Перейти к процедуре обработки сессии
WorkWithSession();
base.Update(gameTime);
}
//Процедура, которая пытается подключиться к существующей сессии
//или создать новую
void TryToJoinOrCreate()
{
//Выводим сообщение в заголовок окна
this.Window.Title = "Ищу доступную сессию";
//Начинаем поиск новой сессии типа SystemLink - такая сессия
//позволяет создавать игры для автономных, не подключенных к Интернету,
//локальных сетей
using (AvailableNetworkSessionCollection availNetSessions =
NetworkSession.Find(NetworkSessionType.SystemLink,
2, null))
{
//Если ни одной сессии нет
if (availNetSessions.Count == 0)
{
this.Window.Title = "Сессия не найдена - создаю новую сессию";
//Создаем новую сессию
networkSession = NetworkSession.Create(NetworkSessionType.SystemLink,
1, 2);
//Подключаем обработчики событий
JoinEventhandlers();
this.Window.Title = "Сессия создана";
}
//Иначе, если сессия найдена
else
{
this.Window.Title = "Сессия найдена, присоединяюсь";
//Присоединяемся к ней и так же подключаем обработчики
networkSession = NetworkSession.Join(availNetSessions[0]);
JoinEventhandlers();
}
}
}
//Подключение обработчиков событий
void JoinEventhandlers()
{
//Объект типа NetworkSession имеет множество событий
//В частности, они используются для того, чтобы показать, что к игре присоединился новый пользователь
//что игра начата, закончен. Здесь мы подключили два события
//Одно из них происходит, когда к игре присоединяется новый игрок, подключим соответствующий обработчик
networkSession.GamerJoined += new EventHandler<GamerJoinedEventArgs>(networkSession_GamerJoined);
//Второе происходит после закрытия сессии
networkSession.SessionEnded += new EventHandler<NetworkSessionEndedEventArgs>(networkSession_SessionEnded);
}
//Обработчик события, происходящего при присоединении нового игрока
void networkSession_GamerJoined(object sender, GamerJoinedEventArgs e)
{
//Получим номер игрока
int plNumber = networkSession.AllGamers.IndexOf(e.Gamer);
//Запишем в поле Tag игрока, которое имеет тип Object, то есть - способно
//принимать любые объекты, новый объект Bat. Номер игрока, который мы передали
//объекту, используется для его установки. В нашей игре могут участвовать лишь два игрока
//поэтому номер 0 используется для установки биты в левой части экрана, а номер 1 -
//в правой. Игрок, создавший сессию, занимает левую часть экрана
e.Gamer.Tag = new Bat(plNumber, Content, this);
}
//Обработчик события, происходящего при закрытии сессии
void networkSession_SessionEnded(object sender, NetworkSessionEndedEventArgs e)
{
//уничтожим сессию
networkSession.Dispose();
networkSession = null;
}
//Процедура обработки событий в течение сессии
void WorkWithSession()
{
//Вызвать процедуру, вычисляющую новое положение игрового объекта
//для локального игрока - в нашем случае это один игрок с номером 0
//и записывающей данные в сетевой поток
CalcAndWrite(networkSession.LocalGamers[0]);
//Обновить состояние сессии
networkSession.Update();
//Если сессия оказалась уничтоженной - выйти из процедуры
if (networkSession == null) return;
//Прочесть сетевые данные и модифицировать состояние объектов
ReadAndImplement(networkSession.LocalGamers[0]);
}
//Вычисления и передача данных в сеть
void CalcAndWrite(LocalNetworkGamer gamer)
{
//Рассматривать объект, записанный в Tag текущего игрока
//как биту - Bat
Bat bat = gamer.Tag as Bat;
//Модифицировать координаты в соответствии с клавиатурными командами
//Клавиша вверх перемещает биту вверх
if (Kb.IsKeyDown(Keys.Up))
{
bat.sprPosition.Y -= 3;
}
//Клавиша вниз - вниз
if (Kb.IsKeyDown(Keys.Down))
{
bat.sprPosition.Y += 3;
}
//Записать положение биты в сеть
packetWriter.Write(bat.sprPosition);
//Если текущий игрок - хост, то есть он создал
//сетевую сессию, на него возложим обязанности
//вычисления позиции мяча и обработки столкновений
if (gamer.IsHost)
{
//Если к игре подключено 2 пользователя
//То есть она началась
if (networkSession.AllGamers.Count == 2)
{
//Изменить позицию мяча в соответствии с его
//скоростью
b.sprPosition += b.sprSpeed;
//Если вышли за пределы верхней части игрового окна
if (b.sprPosition.Y < b.scrBounds.Y)
{
//Вернуть объект и инвертировать скорость по Y
b.sprPosition.Y = b.scrBounds.Y;
b.sprSpeed.Y *= -1;
}
//Если пересекли нижнюю границу окна
//Вернуть объект и инвертировать скорость по Y
if (b.sprPosition.Y + b.sprRectangle.Height > b.scrBounds.Height)
{
b.sprPosition.Y = b.scrBounds.Height - b.sprRectangle.Height;
b.sprSpeed.Y *= -1;
}
//Получить биту для игрока №0 - она расположена слева
//Берем этот объект из списка всех игроков
bat = networkSession.AllGamers[0].Tag as Bat;
//Если есть столкновение с битой
if (b.sprPosition.X + b.sprRectangle.Width > bat.sprPosition.X
b.sprPosition.X < bat.sprPosition.X + bat.sprRectangle.Width
b.sprPosition.Y + b.sprRectangle.Height > bat.sprPosition.Y
b.sprPosition.Y < bat.sprPosition.Y + bat.sprRectangle.Height)
{
//Вернем мяч и инвертируем скорость по X
b.sprPosition.X = bat.sprRectangle.Width;
b.sprSpeed.X *= -1;
}
//Получим биту для игрока №1 - она расположена справа
bat = networkSession.AllGamers[1].Tag as Bat;
//Если есть столкновение с ней
if (b.sprPosition.X + b.sprRectangle.Width > bat.sprPosition.X
b.sprPosition.X < bat.sprPosition.X + bat.sprRectangle.Width
b.sprPosition.Y + b.sprRectangle.Height > bat.sprPosition.Y
b.sprPosition.Y < bat.sprPosition.Y + bat.sprRectangle.Height)
{
//Вернем мяч и инвертируем скорость по X
b.sprPosition.X = b.scrBounds.Width - bat.sprRectangle.Width - b.sprRectangle.Width;
b.sprSpeed.X *= -1;
}
}
//Запишем позицию мяча в сетевой поток
packetWriter.Write(b.sprPosition);
}
//Отправим данные другим игрокам
gamer.SendData(packetWriter, SendDataOptions.InOrder);
}
//Процедура чтения и применения данных от других объектов
void ReadAndImplement(LocalNetworkGamer gamer)
{
//До тех пор, пока есть данные для чтения
while (gamer.IsDataAvailable)
{
//Новый объект sender типа NetworkGamer
//Он бует использован для хранения ссылки на объект
//который отправил данные
NetworkGamer sender;
// Прочесть данные
gamer.ReceiveData(packetReader, out sender);
//Если данные отправлены не локальным игроком - то есть -
//пришли к нам от одного из сетевых игроков (в нашем случае возможно наличие лишь одного
//такого игрока
if (!sender.IsLocal)
{
//Получить объект bat из поля Tag игрока, отправившего данные
Bat bat = sender.Tag as Bat;
//Записать прочитанные данные в качестве позиции этого объекта
bat.sprPosition = packetReader.ReadVector2();
//Если отправитель данных - хост - дополнительно установить
//позицию мяча, прочитав данные из потока
//Таким образом хост всегда пишет в поток два фрагмента данных
//и если другой объект читает данные хоста - он так же
//воспринимает два фрагмента
if (sender.IsHost) b.sprPosition = packetReader.ReadVector2();
}
}
}
protected override void Draw(GameTime gameTime)
{
graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
//Начало вывода спрайтов
spriteBatch.Begin();
//Если создана сетевая сессия и количество игроков равно 2
//То есть - выполнены условия для начала игры
if (networkSession!=null networkSession .AllGamers .Count==2)
{
//Для каждого игрока из всех игроков, имеющих отношение к данной сессии
foreach (NetworkGamer gamer in networkSession.AllGamers)
{
//Получить объект bat для текущего игрока
Bat bat = gamer.Tag as Bat;
//Вывести объект bat
bat.Draw(spriteBatch);
//Вывести мяч
b.Draw(spriteBatch);
}
}
//Завершить вывод объектов
spriteBatch.End();
base.Draw(gameTime);
}
}
}
На рис. 16.4. вы можете видеть экран выбора игрока. Мы зарегистрировались как Player1.
(рис 16.4) Выбор игрокаНа рис. 16.5 вы можете видеть игровой экран после подключения программы к уже созданной сессии.
(рис 16.5) Игровой экран проекта P12_1 Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.