Как сделать движок для футбольного матча? (простой вариант)

RedDeveloper
27.03.2023 12:01
Как сделать движок для футбольного матча? (простой вариант)

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

Итак, что самое главное в футбольной игре? Графика? Может быть. Лицензированные команды и игроки? Безусловно, но есть еще одна вещь, которая важнее этого: движок игры. Да, как вы уже догадались, без самой игры все остальные вещи не имеют смысла. И сегодня я покажу, как сделать движок для футбольного матча.

Фото  Вальдемар  на  Unsplash
Фото Вальдемар на Unsplash

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

И последнее, я хочу сообщить вам, что во время объяснения я буду использовать Java.

Обзор структуры движка

В процессе создания движка я создал и использовал два пакета и четыре класса. Эти пакеты - Action и General.

Пакет Action состоит из классов Scorers и Scoring. Класс Scoring используется для получения точного результата матча. Класс Scorers используется для определения того, какие игроки и в какое время забили эти голы.

Пакет General состоит из классов MatchRunner и TeamData. Класс TeamData используется для сбора в одном объекте информации, относящейся как к домашней, так и к гостевой командам. Класс MatchRunner используется для моделирования матча. Это одна из основных функций движка.

TeamData

TeamData - это неизменяемый класс. Он имеет пять приватных переменных. Это игроки, атака, защита, avgDefence и avgAttack.

private ArrayList<String> players;
private ArrayList<Integer> attack, defence;
private double avgDefence, avgAttack;

Конструктор принимает три параметра. Это игроки, атака, защита.

 public TeamData(ArrayList<String> players, ArrayList<Integer> attack, ArrayList<Integer> defence)
    {
        this.players = players;
        this.attack = attack;
        this.defence = defence;

        average();
    }

Переменная "players" содержит имена игроков команды, которые играют в матче. Переменные "атака" и "защита" содержат данные о способностях игроков атаковать и защищаться.

Кроме того, переменные "avgAttack" и "avgDefence" содержат средние показатели атаки и защиты команды. При инициализации им присваивается значение null, но после получения необходимой информации для их расчета внутри конструктора вызывается функция average.

private void average()
    {
        double total = 0;
        for (Integer p: defence) total += p;
        avgDefence = total / defence.size();

        total = 0;
        for (Integer p: attack) total += p;
        avgAttack = total / attack.size();
    }

Внутри функции среднего значения поочередно вычисляются avgAttack и avgDefence. Чтобы найти среднее значение, необходимо получить сумму способностей. Затем она делится на количество игроков.

Кроме того, класс TeamData включает методы getter, позволяющие получать данные извне класса.

public double getAvgDefence() {return avgDefence;}
public double getAvgAttack() {return avgAttack;}
public ArrayList<String> getPlayers() {return players;}
public ArrayList<Integer> getAttack() {return attack;}

Подсчет очков

Класс Scoring имеет две частные переменные. Это home и away и их тип данных - TeamData. Они присваиваются внутри конструктора.

public Scoring(TeamData home, TeamData away)
    {
        this.home = home;
        this.away = away;
    }

Метод score является ядром симуляции. Потому что он вычисляет, сколько голов забила команда за матч.

public int score(char team)
    {
        int diff = 1;

        if (team == 'h')
        {
            //home advantage
            diff++;

            if (home.getAvgAttack() > away.getAvgDefence()) diff += (int) (home.getAvgAttack() - away.getAvgDefence());
        }

        else if (team == 'a')
        {
            if (away.getAvgAttack() > home.getAvgDefence()) diff += (int) (away.getAvgAttack() - home.getAvgDefence());
        }

        return testChance(diff);
    }

Чтобы сделать это возможным, сначала счет определяет, сколько шансов у команды сделать гол. Эта переменная шанса названа в методе diff, и у каждой команды есть хотя бы один шанс.

int diff = 1;

Кроме того, если команда является домашней, она имеет один дополнительный шанс на гол благодаря преимуществу стадиона.

if (team == 'h')
        {
            //home advantage
            diff++;

Окончательное значение diff вычисляется путем прибавления к diff результата вычитания из средней атаки команды соперника по очкам к средней защите команды соперника (конечно, если средняя атака больше средней защиты).

if (team == 'h')
  {
    //home advantage
    diff++;

    if (home.getAvgAttack() > away.getAvgDefence()) diff += (int) (home.getAvgAttack() - away.getAvgDefence());
  }

 else if (team == 'a')
  {
    if (away.getAvgAttack() > home.getAvgDefence()) diff += (int) (away.getAvgAttack() - home.getAvgDefence());
  }

Наконец, вызывается функция testChances и возвращается окончательный результат.

return testChance(diff);

Функция testChances утверждает, что шанс стать голом составляет 40%.

private int testChance(int chances)
    {
        int goals = 0;

        for (int i = 0; i < chances; i++)
        {
            int faith = new Random().nextInt(5);

            if (faith >= 3) goals++;
        }

        return goals;
    }

Скореры

Класс Scorer состоит из четырех частных переменных. Это home, away, scorerPlayers и minutes.

private TeamData home, away;
private ArrayList<ArrayList<String>> scorerPlayers;
private ArrayList<ArrayList<Integer>> minutes;

Эти переменные присваиваются внутри конструктора.

public Scorers(int[] results, TeamData home, TeamData away)
    {
        this.home = home;
        this.away = away;

        scorerPlayers = new ArrayList<>();
        minutes = new ArrayList<>();

        scorerPlayers.add(new ArrayList<>());
        scorerPlayers.add(new ArrayList<>());
        minutes.add(new ArrayList<>());
        minutes.add(new ArrayList<>());

Переменные scorerPlayers и minutes хранят внутри себя две коллекции ArrayList. Коллекция с индексом 0 предназначена для домашней команды, а следующая коллекция - для информации о гостевой команде.

Затем, если результаты не равны 0, внутри конструктора запускается процесс уточнения.

        if (results[0] != 0)
        {
            getGoalers(false, results[0]);
            setMinutes(false, results[0]);
        }
        if (results[1] != 0)
        {
            getGoalers(true, results[1]);
            setMinutes(true, results[1]);
        }
 }

Метод getGoalers используется для определения игроков, которым были забиты голы. Он принимает параметр в виде количества голов и возвращает функцию who внутри цикла for это количество раз.

private void getGoalers(boolean isAway, int score)
    {
        for (int i = 0; i < score; i++)
        {
            who(isAway);
        }
    }

В начале метод who получает сумму всех возможностей.

int total = getTotalPossibility(isAway);

Метод getTotalPossibility получает данные об атаке домашней или гостевой команды в соответствии с переменной isAway. Затем он возвращает общее значение атакующих возможностей команды.

private int getTotalPossibility(boolean isAway)
    {
        int total = 0;

        ArrayList<Integer> valueList;
        if (!isAway) valueList = home.getAttack();
        else valueList = away.getAttack();

        for (Integer p: valueList) total += p;

        return total;
    }

После получения суммарной возможности метод who получает списки игроков и атакующих способностей в соответствии с переменной isAway.

private void who(boolean isAway)
    {
        int total = getTotalPossibility(isAway);

        ArrayList<String> playerList;
        ArrayList<Integer> attack;
        if (!isAway)
        {
            playerList = home.getPlayers();
            attack = home.getAttack();
        }
        else
        {
            playerList = away.getPlayers();
            attack = away.getAttack();
        }

Затем он выбирает случайное значение от 1 до total.

int point = new Random().nextInt(total) + 1;

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

int ceil = 0;
int floor = 0;
for (int i = 0; i < attack.size(); i++)
  {
    ceil += attack.get(i);
    if (i != 0) floor += attack.get(i-1);

    if (point > floor && point <= ceil)
      {
        int loc = 0;
        if (isAway) loc = 1;
        scorerPlayers.get(loc).add(playerList.get(i));
        break;
      }
  }

Допустим, есть три игрока - игрок1, игрок2 и игрок3. Их способности к атаке равны 40, 50 и 60.

Если 0 < point ≤ 40, то бомбардиром является игрок1.

Если 40 < очков ≤ 90, то бомбардиром является игрок2.

Если 90 < point ≤ 150, то бомбардиром является игрок3.

После определения бомбардиров вычисляется время гола. Для этого вызывается метод setMinutes.

private void setMinutes(boolean isAway, int score)
    {
        ArrayList<Integer> goals = getOrderedMinutes(getRawMinutes(score));

        if (!isAway) for (Integer g: goals) minutes.get(0).add(g);
        else for (Integer g: goals) minutes.get(1).add(g);
    }

Сначала функция setMinutes вызывает метод getRawMinutes, чтобы получить время гола от 0 до 90.

private ArrayList<Integer> getRawMinutes(int score)
{
    ArrayList<Integer> rawMinutes = new ArrayList<>();

    for (int i = 0; i < score; i++) rawMinutes.add(new Random().nextInt(90) + 1);

    return rawMinutes;
}

Затем вызывается функция getOrderedMinutes, чтобы отсортировать их от наименьшего к наибольшему.

private ArrayList<Integer> getOrderedMinutes(ArrayList<Integer> raw)
{
    ArrayList<Integer> ordered = new ArrayList<>();
    ArrayList<Integer> rawEdited = raw;

    while (rawEdited.size() > 0)
    {
        int min = getMin(raw);
        ordered.add(min);
        int id = getMinID(min, rawEdited);
        rawEdited.remove(id);
    }

    return ordered;
}

Функция getOrderedMinutes использует метод getMin, чтобы найти текущий наименьший элемент в списке.

private int getMin(ArrayList<Integer> raw)
    {
        int min = Integer.MAX_VALUE;

        for (Integer r: raw) if (r < min) min = r;

        return min;
    }

Функция getOrderedMinutes также использует метод getMinID для удаления текущего наименьшего элемента в списке.

private Integer getMinID(int number, ArrayList<Integer> raw)
    {
        Integer id = null;

        for (int i = 0; i < raw.size(); i++)
        {
            if (number == raw.get(i))
            {
                id = i;
                break;
            }
        }

        return id;
    }

MatchRunner

Класс MatchRunner имеет пять приватных переменных. Это счет, бомбардиры, минуты, дома и в гостях.

private int[] score;
private ArrayList<ArrayList<String>> scorers;
private ArrayList<ArrayList<Integer>> minutes;

private TeamData home, away;

Конструктор используется для присвоения переменных score (как null), home и away.

public MatchRunner(TeamData home, TeamData away)
{
    score = new int[2];

    this.home = home;
    this.away = away;
}

Метод run внутри класса MatchRunner управляет всеми операциями матча. Классы Scoring и Scorers вызываются внутри функции run.

public void run()
{
    score[0] = new Scoring(home, away).score('h');
    score[1] = new Scoring(home, away).score('a');

    Scorers scorer = new Scorers(score, home, away);
    scorers = scorer.getScorers();
    minutes = scorer.setMinutes();
}

Кроме того, класс MatchRunner включает методы getter и setter для манипулирования переменными и получения результатов извне класса.

public void setHome(TeamData home) {this.home = home;}
public void setAway(TeamData away) {this.away = away;}

public int[] getScore() {return score;}
public ArrayList<String> getScorers(boolean isAway)
{
    if (!isAway) return scorers.get(0);
    else return scorers.get(1);
}

public ArrayList<Integer> getMinutes(boolean isAway)
{
    if (!isAway) return minutes.get(0);
    else return minutes.get(1);
}

Доступность

Photo by  Roman Synkevych 🇺🇦  на  Unsplash
Photo by Roman Synkevych 🇺🇦 on Unsplash

Я выложил исходный код на GitHub. Вы можете посмотреть или развить его по своему усмотрению. Вот ссылка .

Также, вы можете скачать jar файл здесь .

Краткие заметки для пользователей библиотеки

TeamData

Используется для создания объекта команды дома/в гостях.

Параметры конструктора

  • ArrayList<String> игроки
  • ArrayList<Integer> атака
  • ArrayList<Integer> защита

MatchRunner

Внутри него находится функция run.

Параметры конструктора

  • TeamData дома
  • TeamData away

Public void run

Это начальная функция для симуляции матча.

Public int[ ] getScore

Он возвращает счет матча. Длина массива равна двум. int[0] хранит голы хозяев, а int[1] - голы гостей.

Public ArrayList<String> getScorers (boolean isAway)

Возвращает голы, забитые домашней / гостевой командой в соответствии с переменной isAway.

Public ArrayList<Integer> getMinutes (boolean isAway)

Он возвращает минуты голов для команды дома/в гостях в соответствии с переменной isAway.

Public void setHome (TeamData home)

Изменяет переменную home.

Public void setAway (TeamData away)

Изменяет переменную away.

Заключение

Футбол - сложная игра, и нет сомнений, что реализовать ее в компьютерных играх - дело непростое. Но и простое применение игры не является сложным.

В этом примере фолы, красные/желтые карточки, офсайд, свободный удар и т.д. не были добавлены, чтобы не усложнять ситуацию. На данный момент любой может добавить их в движок и управлять ими по своему усмотрению. Вот код, вот возможность. Остальное зависит от вас.

Photo by  Humberto Santos  на  Unsplash
Photo by Humberto Santos on Unsplash
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?

20.08.2023 18:21

Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в 2023-2024 годах? Или это полная лажа?".

Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией

20.08.2023 17:46

В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.

Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox

19.08.2023 18:39

Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в частности, магию поплавков и гибкость flexbox.

Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest

19.08.2023 17:22

В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для чтения благодаря своей простоте. Кроме того, мы всегда хотим проверить самые последние возможности в наших проектах!

Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️

18.08.2023 20:33

Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий их языку и культуре.

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL

14.08.2023 14:49

Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип предназначен для представления неделимого значения.