Я продолжаю развивать механику сундуков и того, как гейм дизайнеры будут их заводить и создавать. В прошлой статье мы говорили о идее того, как можно реализовать награды и сундуки, так что бы в будущем можно было удобно добавлять новые типы наград и расширять систему. Мы не будем рассматривать пока экономику игры и то как сделать сундуки покупаемыми и как правильно балансить и рандомить их.
В данной статье мы пройдемся уже по примерам кода и попробуем написать рабочий прототип - от заведения награды гейм дизайнером, до выдачи этой награды в игре. Будет много примеров кода, и мне очень хотелось бы получить фидбэк от тех кто дочитает до конца. Можно посмотреть примеры кода в 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?