Как стать автором
Обновить

Награды в играх. Вариант backend реализации

Время на прочтение5 мин
Количество просмотров2.6K

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

В данной статье мы пройдемся уже по примерам кода и попробуем написать рабочий прототип - от заведения награды гейм дизайнером, до выдачи этой награды в игре. Будет много примеров кода, и мне очень хотелось бы получить фидбэк от тех кто дочитает до конца. Можно посмотреть примеры кода в GitHub.

Ну что ж приступим. Для начала создадим таблицу “Награда” в нашей системе. Это корневая таблица в которой будут храниться все игровые награды. И нам конечно потребуется “энтити” к этой таблице. Ниже приведен код базовой сущности. 

@Getter
@Setter
@Entity(name = "reward")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "type",
        discriminatorType = DiscriminatorType.STRING)
@DynamicUpdate
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE, include = "non-lazy")
public abstract class Reward implements GameItem {

    /**
     * Идентификатор сущности
     */
    @Id
    @GeneratedValue(
            strategy = GenerationType.SEQUENCE,
            generator = "seq_reward"
    )
    @SequenceGenerator(
            name = "seq_reward",
            allocationSize = 1
    )
    @Column(name = "id", nullable = false, length = 36, unique = true)
    private Long id;

    @Column(name = "alias", length = 255)
    private String alias; // short description

    @Column(name = "min_count")
    private Integer minCount = 0; // it is diff card count for card reward, can be null for some rewards

    @Column(name = "max_count")
    private Integer maxCount = 0; // it is diff card count for card reward, can be null for some rewards

И создадим более конкретную награду "золото" ("GOLD").

@Entity
@DiscriminatorValue("GOLD")
@NoArgsConstructor
public class GoldReward extends Reward {

    /**
     * Generate gold currency for profile
     */
    @Override
    public List<?> rewardFor(Profile profile, ContentGenerator contentGenerator) {
        int count = contentGenerator.generateCount(this.getMinCount(), this.getMaxCount(), this.getCount());
        return List.of(new GoldCurrency(profile, count));

    }

    @Override
    public RewardType getType() {
        return RewardType.GOLD;
    }

Можно заметить что GoldReward возвращает тип RewardType.GOLD. Это enum всех типов наград. В целом это может быть строка, чтобы в будущем небыли проблем с расширением этого класса.  

public interface GameItem {
    List<?> rewardFor(Profile profile, ContentGenerator contentGenerator);
}
INSERT INTO reward (id, alias, type, min_count, max_count)
VALUES (1, 'Золото немного', 'GOLD', 30, 60);

INSERT INTO reward (id, alias, type, min_count, max_count)
VALUES (2, 'Золото среднее', 'GOLD', 100, 200);

INSERT INTO reward (id, alias, type, min_count, max_count)
VALUES (3, 'Золото большое', 'GOLD', 500, 700);

И добавим репозиторий для получения наград на сервере. Я использую Spring Data для этого.

public interface RewardRepository<T extends Reward> extends JpaRepository<T, Long> {
}

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

public interface ContentGenerator {

    /**
     * Should drop reward
     */
    boolean shouldDropReward(int chance);

    /**
     * Generate random count in interval
     */
    int generateCount(Integer from, Integer to, Integer count);
}

Я привожу несколько вариантов взятия награды. Мы пока сосредоточимся на получении награды по ее id. 

public class RewardServiceImpl implements RewardService {

    @Override
    @SneakyThrows
    public List<Object> claimReward(long rewardId) {
        Profile currentProfile = profileService.getCurrentProfile();
        Reward reward = (Reward) rewardRepository.findById(rewardId).orElseThrow(() -> new RuntimeException("Reward not found"));
        return claimReward(currentProfile, reward);
    }

    @Override
    @SneakyThrows
    public List<Object> claimReward(Profile profile, Reward reward) {
         List<?> rewards = reward.rewardFor(profile, contentGenerator);
        Map<Class, GameContentService> mapContentServices = gameContentServices
                .stream()
                .collect(Collectors.toMap(GameContentService::getSupportedType, gameContentService -> gameContentService));
        List<Object> resultList = new ArrayList<>();

        /*
         * Save and apply reward for profile
         */
        processReward(rewards, mapContentServices, resultList);
        return resultList;
    }
}

Для того чтобы применить награду на профиль нам потребуются различные GameContentService. 

public interface GameContentService<T> {

    /**
     * Save or update content to profile
     */
    T saveOrUpdate(T content);

    /**
     * Get supported type of Service, for add reward to profile
     */
    Class<T> getSupportedType();

}

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

В целом это все что необходимо в такой архитектуре, для создания и применения награды. Не сложно заметить, что достаточно удобно докидывать новые типы наград и процессоры для них. Мы по сути не меняем старый код (кроме одного enum, от которого в целом можно отказаться). Поддерживается принцип open-close. 

Что думаете по поводу такого решения? Как у вас реализован такой механизм на backend?

Теги:
Хабы:
Всего голосов 5: ↑5 и ↓0+5
Комментарии8

Публикации

Истории

Работа

Java разработчик
443 вакансии
Unity разработчик
13 вакансий

Ближайшие события

19 августа – 20 октября
RuCode.Финал. Чемпионат по алгоритмическому программированию и ИИ
МоскваНижний НовгородЕкатеринбургСтавропольНовосибрискКалининградПермьВладивостокЧитаКраснорскТомскИжевскПетрозаводскКазаньКурскТюменьВолгоградУфаМурманскБишкекСочиУльяновскСаратовИркутскДолгопрудныйОнлайн
24 – 25 октября
One Day Offer для AQA Engineer и Developers
Онлайн
25 октября
Конференция по росту продуктов EGC’24
МоскваОнлайн
26 октября
ProIT Network Fest
Санкт-Петербург
7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань