Pull to refresh

Как написать игру на Monogame, не привлекая внимания санитаров. Часть 2, натягиваем спрайты на глобус

Reading time5 min
Views3.9K

Предыдущие части: Часть 0, Часть 1

4.4 Создаем визуальное оформление

В прошлый раз мы остановились на том, что сделали белый квадратик, перемещающийся по голубому фону. Настало время сделать так, чтобы наша игра стала хотя бы визуально похожей на игру. Сначала сделаем разрешение побольше, чтобы было удобно работать. Так как я люблю классику, то ставлю максимальное разрешение, которое было на моем самом первом мониторе в классе View.

//View
protected override void Initialize()
{
  base.Initialize();
  _graphics.IsFullScreen = false;
  _graphics.PreferredBackBufferWidth = 1024;
  _graphics.PreferredBackBufferHeight = 768;
  _graphics.ApplyChanges();
}

Обратите внимание, что для того, чтобы изменения вступили в силу, нужно вызвать у объекта _graphics метод ApplyChanges. Без этого метода волшебства не случится.

Следующим шагом сделаем машинку. Чтобы не иметь проблем с правообладателями и не тратить время, нарисуем ее сами. Я выбрал такой дизайн:

Если вы узнали эту картинку, значит, вам пора считать свой пенсионный стаж. Это тоже заглушка, в конце мы достанет нормальные спрайты машинок, но визуально такое решение выглядит намного приятнее, чем раньше. Добавляем ее в ресурсы на место White Placeholder.

Немного избавимся от хардкода и добавим механику автоматической генерации ключей объекта в методе Initialize нашей Модели, так как все-таки подразумевается, что у нас будет не один объект на карте, а много.

public class GameCycle : IGameplayModel
{
    public event EventHandler<GameplayEventArgs> Updated = delegate { };

    private int _currentId; //Добавляем поле свободного ключа для объекта

    public int PlayerId { get; set; }
    public Dictionary<int, IObject> Objects { get; set; }
   
    public void Initialize()
    {       
        Objects = new Dictionary<int, IObject>();
       _currentId = 1; //Инициализируем свободный ключ
       Car player = new Car();
       player.Pos = new Vector2 (512-90, 500);
       player.ImageId = 1;
       player.Speed = new Vector2 (0, 0);
       //Добавляем объект в словарь с использованием нового поля
       //и осуществляем инкрементацию ключа
       Objects.Add (_currentId, player);
       PlayerId = _currentId;
       _currentId++;
    }

Магические числа в размещении игрока new Vector2 (512-90, 500) появились для размещения машинки по центру экрана. 90, так как ширина спрайта у меня равна 180. Позднее от хардкода здесь и на других участках нужно будет избавиться. Выглядеть это будет следующим образом (чтобы получить такой же цвет фона во View в методе Clear объекта GraphicsDevice нужно поставить DarkSeaGreen):

На мой взгляд, теперь это хотя бы не выглядит, как голая заготовка. Похоже на нечто, отдаленно напоминающее игру. Однако, если запустить нашу машинку вперед, она уйдет за край экрана, и больше мы ее не увидим никогда. Необходимо, чтобы наше поле зрения, следовало за машинкой. То есть, настало время сделать камеру.

На самом деле, в MonoGame есть класс Matrix, из которого можно собрать камеру. Существует достаточно много источников, где показано, как это сделать как для трехмерных, так и для двухмерных случаев. Но, как мне кажется, в данном случае это слишком сложно для такой простой двухмерной игры, поэтому мы соберем камеру саму из простой пластиковой бутылки.

Для начала обращу ваше внимание на следующее: логическая часть и отрисовка четко разделены (чего я с самого начала и добивался) — неважно, как мы меняем координату нашей машинки внутри логики — чтобы она появилась на экране нужно дать отдельную команду _spriteBatch.Draw, в которой указывается, что мы рисуем и где. Иными словами, координаты объекта и координаты, где мы его нарисуем на экране, для программы представляют собой разные сущности. Они могут совпадать, а могут не совпадать. Чтобы поменять поле зрения достаточно сделать так, чтобы они не совпадали. Следующий вопрос — на сколько они не должны совпадать? Так как наша камера должна двигаться так, чтобы машинка у нас на экране всегда была неподвижной, то, очевидно, координаты, в которых мы рисуем объекты, должны смещаться относительно своего реального положения на величину, на которую сместилась машинка игрока относительно своего предыдущего положения.

Итак, объявим в классе GameCycleView новое приватное поле _visualShift, которое и будет показывать, на сколько нужно сместить объект при отрисовке, а в его методе Draw вычтем значение этого поля из позиции отрисовки:

public class GameCycleView : Game, IGameplayView
{
	//<.....................>
  private Vector2 _visualShift = new Vector2(0, 0);
  //<.....................>
  protected override void Draw(GameTime gameTime)
  {
    GraphicsDevice.Clear(Color.DarkSeaGreen);
    base.Draw(gameTime);
    _spriteBatch.Begin();
    
    foreach (var o in _objects.Values)
    {
    	_spriteBatch.Draw(
      _textures[o.ImageId], 
      o.Pos - _visualShift/*смещение позиции отрисовки*/,
      Color.White
      );
    }
    _spriteBatch.End();
  }    
}

Таким образом, мы вычитаем из позиции объекта вектор _visualShift, в который будем каждый цикл записывать сдвиг машинки. Почему вычитаем? Допустим, машинка проехала на 10 пикселей вверх. Если гипотетическая камера хочет оказаться относительно машинки на той же позиции, что и раньше, то можно сказать, что камера неподвижна, а весь остальной мир спустился относительно нас на 10 пикселей вниз.

Заготовка во View готова, нужно связать ее с логической частью. Делать мы это будем через событие обновления игрового цикла. Добавим в наш класс GameplayEventArgs новое поле POVShift, в которое и будем записывать смещение относительно предыдущего цикла:

public class GameplayEventArgs : EventArgs
{
    public Dictionary<int, IObject> Objects { get; set; }
    public Vector2 POVShift { get; set; }
}

В модели же посчитаем эту величину смещения (создав в методе Update переменную playerInitPos, в которую сохраним положение игрока в начале цикла и затем вычтя ее из положения в конце) и передадим через событие Updated:

public void Update()
{
  Vector2 playerInitPos = Objects[PlayerId].Pos;
  foreach (var o in Objects.Values)
  {
    o.Update();
  }
  Vector2 playerShift = Objects[PlayerId].Pos - playerInitPos;
  Updated.Invoke(this, new GameplayEventArgs { 
    Objects = this.Objects, POVShift = playerShift
    } );
}

Так как все объекты у нас хранятся в словаре, а айдишник игрока мы всегда знаем, вытянуть параметры игрока можно легко и быстро.

Осталось сделать так, чтобы View принимала новый параметр, для чего меняем метод LoadGameCycleParameters в интерфейсе IGamePlayView, его вызов в презентере и реализацию данного метода в GameCycleView.

public interface IGameplayView
{
    event EventHandler CycleFinished;
    event EventHandler<ControlsEventArgs> PlayerSpeedChanged;
   
  	void LoadGameCycleParameters(
      Dictionary<int, IObject> _objects, 
      Vector2 POVShift
    );
    void Run();
}
//Presenter
private void ModelViewUpdate(object sender, GameplayEventArgs e)
{
    _gameplayView.LoadGameCycleParameters(e.Objects, e.POVShift);
}
//View
public void LoadGameCycleParameters(Dictionary<int, IObject> Objects, Vector2 POVShift)
{
    _objects = Objects;
    _visualShift += POVShift;
}

Теперь, если мы запустим программу, то увидим что машинка при нажатии клавиш не исчезает. Но без других объектов непонятно, двигается она или нет. Добавим еще один объект, чтобы это стало очевидно — создадим еще одну машинку в нашем методе инициализации игрового цикла:

public void Initialize()
{
    Objects = new Dictionary<int, IObject>();
    _currentId = 1; //Инициализируем свободный ключ
    Car player = new Car();
    player.Pos = new Vector2(512 - 90, 500);
    player.ImageId = 1;
    player.Speed = new Vector2(0, 0);    
    Objects.Add(_currentId, player);
    PlayerId = _currentId;
    _currentId++;
  
  	Car anotherCar = new Car();
  	anotherCar.Pos = new Vector2(200, 50);
    anotherCar.ImageId = 1;
    anotherCar.Speed = new Vector2(0, 0);
  	Objects.Add(_currentId, anotherCar);
  	_currentId++;
}

Итак, камера работает, как надо, и полностью включена в нашу структуру. В следующий раз мы сделаем простейший механизм создания карты трассы и генерации объектов по ней.

Спасибо за внимание!

Tags:
Hubs:
+7
Comments6

Articles

Change theme settings