Эта лабораторная работа посвящена созданию сетевых игр с помощью XNA.
Цель работы
Научиться создавать сетевые многопользовательские игры
Задачи работы
Изучить модели сетевого взаимодействия игр
Познакомиться с поддержкой сервиса Microsoft LIVE для Windows-игр
Ознакомиться с объектами XNA, предназначенными для организации сетевого взаимодействия игр
Создать сетевую игру – клон игры Pong.
Модели сетевого взаимодействия компьютерных игр
Существуют две основные модели взаимодействия сетевых игр.
Первая модель – это Peer-to-peer – то есть, архитектура, основанная на взаимодействии равноправных программ. Такая модель взаимодействия предусматривает равноправие каждой из запущенных копий игры. При подобном способе взаимодействия каждая из запущенных копий игры равноправна, она обязана отслеживать состояние других копий, отправлять каждой из них сообщения. Такая модель взаимодействия подходит лишь для игр, предусматривающих небольшое количество игроков – при росте количества лавинообразно увеличивается поток сообщений, которыми они вынуждены обмениваться для поддержания игрового процесса. Фактически, каждая из копий игры связана с каждой из других копий. Схематически модель взаимодействия peer-to-peer вы можете видеть на рис. 12.1.
Рис. 12.1. Модель взаимодействия Peer-to-Peer
Как правило, в такой модели взаимодействия одна из игр назначается хостом – именно к ней должны обратиться новые игроки для того, чтобы принять участие в игре. Однако хост не управляет обменом сообщениями между играми
Вторая модель сетевого взаимодействия игр – это модель Client/Server (Клиент/Cервер). При таком взаимодействии выделяется два вида программ. Первая – это программа-сервер, на которую ложатся все заботы по организации игры и по взаимодействию с программами-клиентами. При таком подходе клиенты обмениваются сообщениями лишь с сервером, не взаимодействия друг с другом напрямую. Этот подход позволяет создавать игры, эффективно использующие пропускную способность каналов связи и поддерживающие одновременное участие в игре множества игроков. На рис. 12.2. вы можете видеть схематически изображенную модель взаимодействия Client/Server.
Рис. 12.2. Модель взаимодействия Client/Server
Каждая из игр одноранговой архитектуры является самостоятельной единицей, содержащей в себе все необходимое для полноценной организации игрового процесса. Фактически, каждая из копий игры работает самостоятельно, отсылая другим копиям информацию о своем состоянии и получая от других игр данные об их состоянии. При клиент-серверной схеме взаимодействия возможны несколько подходов. При первом каждый клиент является полноценной программой, которая выполнят все необходимые вычисления и отсылает серверу лишь их результаты. В итоге сервер превращается в инструмент для рассылки информации о состоянии игр между подключенными к нему клиентами. Второй подход заключается в большей нагруженности сервера – так, например, все игровые вычисления могут вестись на сервере, а клиенты используются лишь для приема ввода пользователя и отправки его на сервер, а так же для визуализации принятых от сервера данных.
Возможны и другие модели организации сетевого взаимодействия игр. Например – кольцевая система обмена сообщениями или модель, предусматривающая наличие нескольких игровых серверов.
Виды сетевых игр
Сетевые игры можно подразделить на два вида. Первый вид – это пошаговые игры (Turn Based Games). Яркий пример подобных игр – шахматы, шашки, компьютерные представления настольных игр. Игрок имеет определенное время на то, чтобы принять решение об очередном действии, ходе, а другие игроки вынуждены ждать своей очереди. Такие игры хорошо переносят проблемы сетевой связи – задержка прибытия сообщения на несколько секунд не слишком испортит впечатление от неторопливого шахматного турнира.
Второй вид игр – это игры реального времени (Real Time Games). К таким играм относится, например, популярная Counter Strike. Действия игроков происходят в реальном времени – даже небольшая проблема со связью способна очень сильно помешать игровому процессу.
Многие современные игры, не поддерживающие игру по сети, предусматривают хранение результатов игр на специальных интернет-сервисах фирм-производителей игр, позволяя игрокам соревноваться друг с другом «заочно». XNA поддерживает различные способы организации сетевого взаимодействия игр.
Сетевые сервисы XNA
Мы отметим здесь два основных вида сетевого взаимодействия игр, которые поддерживает XNA.
Первый вид – это игры, которые используют для взаимодействия сервис Microsoft LIVE. Например – одна из моделей взаимодействия через LIVE организуется с помощью сессии типа NetworkSessionType.PlayerMatch. Для того, чтобы играть в такие игры, нужно подключение к Интернету, нужно получить учетную запись на сервере LIVE. Этот вид сетевых игр позволяет объединять игроков без учета их территориальной расположенности или других подобных ограничений.
Второй вид – это игры, которые могут взаимодействовать лишь в локальной сети. Эта модель взаимодействия реализуется с помощью сетевой сессии типа NetworkSessionType.SystemLink. Для организации игры не нужно подключение к Интернету, LIVE-аккаунт. Эта модель взаимодействия хорошо подходит для простых игр, однако, если вы хотите серьезно заниматься разработкой сетевых игр на платформе XNA – ориентируйтесь на LIVE-игры.
Рассмотрим последовательность создания сетевой составляющей компьютерной игры.
Особенности организации сетевой игры
Для того чтобы создать сетевую игру, нужно выполнить следующие шаги:
Создать учетную запись игрока, войти в систему
Создать сетевую сессию
Дождаться подключения игроков
Начать игру
В процессе игры вам придется обновлять состояние игровых объектов, следить за действиями игроков, проверять игру на выполнение критериев окончания, а так же – выполнять другие игровые задачи.
Рассмотрим создание сетевой SystemLink-игры, которая может работать в локальной сети.
Разработка сетевой игры
Разработаем сетевую версию клона популярной игры Pong. Реализуем в нашем примере базовую функциональность сетевой игры, которая, при практическом применении, должна быть дополнена.
Создадим новый игровой проект P12_1. На рис. 12.3. вы можете видеть окно Solution Explorer для этого проекта.
Рис. 12.3. Окно Solution Explorer для проекта P12_1
Здесь мы использовали уже знакомый вам набор текстур BallandBats, содержащий текстуры для клона игры в Pong. Основная функциональность сосредоточена в классе Game1. Классы Ball и Bat используются для представления соответствующих объектов в игре.
Алгоритм работы с программой выглядит так. Сразу после запуска она запускает интерфейс для создания учетной записи игрока, после того, как игрок войдет в систему, запускается игра. После запуска программа пытается автоматически найти сетевую сессию, созданную другой аналогичной программой. Если попытка поиска не увенчалась успехом, программа самостоятельно создает сессию и ждет подключения другой программы. После подключения начинается игра.
В листинге 12.1. вы можете найти код класса Ball. Этот класс используется для хранения атрибутов мяча, а так же – содержит код для вывода объекта на экран.
usingSystem;usingSystem.Collections.Generic;usingMicrosoft.Xna.Framework;usingMicrosoft.Xna.Framework.Audio;usingMicrosoft.Xna.Framework.Content;usingMicrosoft.Xna.Framework.GamerServices;usingMicrosoft.Xna.Framework.Graphics;usingMicrosoft.Xna.Framework.Input;usingMicrosoft.Xna.Framework.Net;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//в цикле вывода изображений с помощью объекта типа SpriteBatchpublicvoid Draw(SpriteBatch spriteBatch){ spriteBatch.Draw(sprTexture, sprPosition, sprRectangle, Color.White);}}}
В листинге 12.2. вы можете найти код класса Bat. Он почти так же прост, как и код мяча. Единственное существенное отличие – он включает в себя проверку на столкновение с верхней и нижней границами экрана. Эта проверка происходит каждый раз после вызова процедуры вывода объекта на экран.
usingSystem;usingSystem.Collections.Generic;usingMicrosoft.Xna.Framework;usingMicrosoft.Xna.Framework.Audio;usingMicrosoft.Xna.Framework.Content;usingMicrosoft.Xna.Framework.GamerServices;usingMicrosoft.Xna.Framework.Graphics;usingMicrosoft.Xna.Framework.Input;usingMicrosoft.Xna.Framework.Net;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);}//Процедура визуализации, которую вызываем из основной программыpublicvoid 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);}}}
Теперь рассмотрим код главного класса нашей игры. В листинге 12.3. вы можете найти код класса Game1. Именно он реализует сетевую функциональность для нашей игры. При разработке этого класса мы пользовались справочными материалами Microsoft.
usingSystem;usingSystem.Collections.Generic;usingMicrosoft.Xna.Framework;usingMicrosoft.Xna.Framework.Audio;usingMicrosoft.Xna.Framework.Content;usingMicrosoft.Xna.Framework.GamerServices;usingMicrosoft.Xna.Framework.Graphics;usingMicrosoft.Xna.Framework.Input;usingMicrosoft.Xna.Framework.Net;namespace P12_1{publicclass 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));}protectedoverridevoid Initialize(){//Объекты для заиси и чтения данных созданы//с конструкторами по умолчанию packetWriter =new PacketWriter(); packetReader =new PacketReader();base.Initialize();}/// protectedoverridevoid LoadContent(){ spriteBatch =new SpriteBatch(GraphicsDevice);//Создадим новый мяч b =new Ball(Content, this);}protectedoverridevoid UnloadContent(){// TODO: Unload any non ContentManager content here}protectedoverridevoid 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.Tagas 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;}//Если пересекли нижнюю границу окна//Вернуть объект и инвертировать скорость по Yif(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].Tagas 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].Tagas 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.Tagas Bat;//Записать прочитанные данные в качестве позиции этого объекта bat.sprPosition= packetReader.ReadVector2();//Если отправитель данных - хост - дополнительно установить//позицию мяча, прочитав данные из потока//Таким образом хост всегда пишет в поток два фрагмента данных//и если другой объект читает данные хоста - он так же//воспринимает два фрагментаif(sender.IsHost) b.sprPosition= packetReader.ReadVector2();}}}protectedoverridevoid 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.Tagas Bat;//Вывести объект bat bat.Draw(spriteBatch);//Вывести мяч b.Draw(spriteBatch);}}//Завершить вывод объектов spriteBatch.End();base.Draw(gameTime);}}}
На рис. 12.4. вы можете видеть экран выбора игрока. Мы зарегистрировались как Player1.
Рис. 12.4. Выбор игрока
На рис. 12.5 вы можете видеть игровой экран после подключения программы к уже созданной сессии.
Рис. 12.5. Игровой экран проекта P12_1
Вопросы
1) Что характерно для сетевой архитектуры Per-To-Peer?
a. Выделенная программа, которая управляет игрой
b. Равноправие запущенных экземпляров игры – если текущий хост оказался неработоспособным – одна из запущенных копий сможет сыграть его роль
c. Невозможность работы в глобальных сетях
d. Невозможность работы в локальных сетях
2) Каков основной недостаток архитектуры Peer-To-Peer?
a. Невозможность работы в глобальных сетях
b. Невозможность работы в локальных сетях
c. Сложность реализации
d. Быстрый рост нагрузки на системные ресурсы при увеличении количества игроков
3) Какие функции выполняет программа-сервер в клиент-серверной архитектуре?
a. Она отвечает за загрузку графических ресурсов
b. Она отвечает за организацию игры, за взаимодействие с программами-клиентами
c. Она используется непосредственно для игры
4) Какие функции выполняет программа-клиент в клиент-серверной архитектуре
a. Она отвечает за загрузку графических ресурсов
b. Она отвечает за организацию игры, за взаимодействие с программами-клиентами
c. Она используется непосредственно для игры
5) Сетевая сессия типа NetworkSessionType.SystemLink позволяет создавать игры, в которые можно играть…