Author Archives: vladimirslav

Scam Key Requests on Steam: Who Hides Behind Fake Emails

You know, I have plenty of reasons to enjoy releasing new games. I am sure you can relate to most of them: anticipation of warm reception, fan excitement, monetary gain. But one rather unconventional thing is my anticipation of scam emails. When I’ve started publishing games two years ago, I quickly figured out that people will try to scam you out of the keys. I simply ignored them or asked for verification and they never replied. But since about a year ago, I’ve actually started asking them to share their reasons and insights. I’ve learned a few things that I think could benefit you too if you are publishing games on Steam.

Group of Egrets by Ohara Koson. The images are just here to make text easier to read. Digitally enhanced by rawpixel.

Group of Egrets by Ohara Koson. The images are just here to make text easier to read. Digitally enhanced by rawpixel.

Background

The usual conversation starts like this:

Scammer:Hi [studio name in best case, generic greeting at worst], I’m a huge youtuber and I need a few keys [first red flag] to review the game.
Me[after doing checks and figuring out it’s a scam]: Oh sure, can I ask a few questions then?
Scammer[best case]:Sure
Me:

Great!
Since original [youtuber] email is different, I know that you are pretending to be someone you are not.

That’s fine, I’m not judging you for that. I always wanted to ask some things (in non-agressive way) to people who try to get keys this way. If you have some time, do you mind answering a few questions for the interviewing purposes (anonymity guaranteed of course)?

1) Is this more of a hobby or does it actually sustains your living?
2) If you sell those on platforms like g2a/kinguin/etc – are they trying to identify you in any way? Have you ever faced repercussions for their side (i.e. if dev sent you a bad key)?

Thanks for your time,
Vladimir

___

After that, if they reply, the conversation starts. I’ve spent about a year gathering these replies, trying to persistently follow-up some people for 10+ times. My small research outlines the reasons why people do it. Since my native language is Russian (and a lot of these people come from from Russia), it helped when I addressed them in native language too. The replies are translated. The research group is small: out of 20 emails I’ve responded, 6 answered and only 4 of them gave interesting answers. Three out of four replies were in Russian, I’ve translated them. Here are the cases and reasons below:

Crows at full moon (1925 - 1936) by Ohara Koson (1877-1945).

Crows at full moon (1925 – 1936) by Ohara Koson (1877-1945).

Case 1: codename “anonymous

Letter not quoted directly by request [translated from russian + word order changed in some places].

Hi Vladimir.

For most people, me included, these shenanigans is, of course, a hobby. We just want to fill our libraries and exchange keys to get other games. For this to be profitable – you’ll have to make it a full-time job.

That means spending 10+ hours a day spending on boring routine: finding emails, names, registering fake mail accounts, working with key markets. This could bring significant income earlier, but now there are so many of us and markets are charging ridiculous percentages. To earn around $1000 a month now, you’d have to spend tons of time on routine. Most of the keys are getting exchanged by collectors. I feel like the used key markets only get a small amount of scammed keys (that were sent to fake press).

Speaking about key markets like g2a or kinguing: their owners are on a sketchy path. On one side, most of their profits are from those “gray” keys and they perfectly understand that: self-respecting publishers and developers don’t want to cooperate, but legal key sellers can’t compete with “gray” keys because they can infinitely underbid them and still turn profit. The platforms have to publicly show their honesty and fight against fake key resellers, but in truth they do these things half-heartedly, since if they move the platforms towards legal key sales – their profits will drop significantly and someone else will take the niche.

Regarding the fines from the gray key markets: it’s against their best interests. If the trader’s every second or third key is fake – he will get banned, but this probably does not happen often. Maybe there are 5% of problematic key sales, so platforms and traders can easily refund the money to the buyer. One interesting thing is that since there is no official check whether the key is legit, some buyers actually purchase the key, activate it on their other accounts and send the support request about receiving a fake key. G2a and Kinguin almost always take the customer’s side, that’s why sellers literally “gift” the keys to the buyers. That’s one more reason why traders are not fined and legal key sellers have no reason to be there.

It’s not surprising that not many people respond to you. Most of them are just kids and not seasoned criminals, who think “it does not cost anything for developers – but we won’t buy the game anyway so it’s not theft and not a loss of profit.” No surprise that these types of talks make people feel guilty.

Long-eared owl on bare tree branch (1900 - 1930) by Ohara Koson

Long-eared owl on bare tree branch (1900 – 1930) by Ohara Koson

Case 2, codename “Rome”:


> Ok Vladimir

For me it’s a hobby, I collect games. Finance does not allow to buy a

lot of games, My friend advised to do so

Thank you.

>Got it, thanks for your reply! Why not just pirate them from torrents?

>Because I do not play games, I just collect them on Steam account.

It’s just a hobby

>If you are just collecting them, why do you need multiple keys?


>For exchanges with other collectors


Not much, but at least the explanations were clear.

Chicken and cock (1900 - 1936) by Ohara Koson (1877-1945)

Chicken and cock (1900 – 1936) by Ohara Koson (1877-1945)

Case 3, codename “Trader


> Hey. OK, I’ll answer.
1) 50-50.
It brings me hefty sums of money. It’s not my main source of income, since I still have a youtube channel where I create videos or dubs. I also own a few steam curator groups. It’s just a way for me to get some more money for more serious projects. I have a few original twitch ideas which are in development. In short: I want to stop doing it as soon as possible and move on to my own projects before the end of the year.
2) When you are registering on g2a / kinguin, you always need to show a source where the keys are coming from. The systems are different on both platforms. i.e. G2A sometimes asks for “screens” describing where the key comes from, but this is easily circumvented if you have some experience behind your back. Apart from that, the fake key shops usually don’t have names (that match youtubers name used in introduction) and the emails are different anyway.
2.1)
The fines occasionally happen when developers send bad keys instead of simply ignoring us. Sometimes this can be noticed (developers tend to send their messages this way, I think I don’t need to explain the meaning of those), but often the other keys go straight for sale. The consequences are very different on both platforms: if key has been purchased and it did not work, you just give a chargeback. But if this repeats, say, 5 times a week, you get fined. Also, if we receive many keys in one letter, we check one for verification purposes.
If you have something else to ask – fire away.

Me:

Thank you! Since you’ve offered, here’s a few more:
1) if it’s not too personal: what does a “hefty sum of money” means? Everyone has a different definition: are we talking about thousands / tens of thousands / hundreds of thousands roubles? Not asking for exact sum, more like a range.
2)
Are there any groups of people that do it collectively? Maybe you have some contacts that I can get more interviews? Pretty hard to get answers from other fake emails.

Reply:

> 1) It depends how “boldthe user of this method is. I know a man who could move to other country and comfortably live there thanks to this, having this as mostly his main source of income. If you spam a lot of people from 3-5 different addresses – you can get an idea how many keys you can get. I’ll tell it from my own experience: 400-600 EUR is a pretty average monthly income, especially if you have your own curator groups. If someone has lots of them and works tirelessly – 1000 EUR is definitely reachable.
2) I don’t think you’ll ever find these,
and I don’t think such communities exists. Even if they do – these are going to be closed-type things.

Another thing is that the digital key has no price, since you can generate it for free. That’s why it does not seem like such a horrible thing. You sell the key, the game gets new audience. The only thing is that you, developers, are not going to get a percentage, which makes it mostly your problem.

Songbird on blossom branch (1900 - 1936) by Ohara Koson (1877-1945)

Songbird on blossom branch (1900 – 1936) by Ohara Koson (1877-1945)

Case 4, codename “Not Really A Scammer”

this was actually a two very similar curator group requests for keys. I’ve compared them and they had the same owners, hence I sent my “clarification/you are a scam” email. While it’s not technically a scam, I still think it teaches a few things about curation circles.

> Hello, Vladimir. To be honest, you are making your conclusions based on inherently wrong assumptions, because you do not understand who creates curator groups and for what purposes. In truth, Curator Groups are a poor way for key acquisition.

Promoting them takes money and 90% of the games we are getting through curator connect. Those keys can not be given to other people: they can only be accepted by group owner and moderators. That’s why it’s one of the purposes why some of the curation groups have similar owners and moderators. The developers often send multiple keys of the game so owners of the curation groups (and you have to understand that these people often know each other since they are all into game collecting) give each other access to redeem the extra copies between themselves. It does not mean that curators are trying to cheat the developers: developers get honest reviews, but curators get +1 game to their library + some more game copies that can be exchanged with other group owners. Apart from that, there are “hired reviewers” – people that are not owners / moderators of curator groups, but just help to write reviews. They get keys to write content for the curator group, thus reducing our workload.
In conclusion, I think you are mistaken in your guess. Curator groups can’t really make money on game reselling, since most games are given not as keys, but as curator connect copies. The expenses for promoting and maintaining the group are higher than potential income from selling the games. People who sell keys do not create curator groups, but at most can exchange their keys with other curators for less “liquid” game.

I’ll expand my previous explanation so you can get the proportions of this: you think everything is based on profiteering, but it’s a mistake. Everything is based on collecting games. I am number X[calls his position, very close to top, double digit number] of user rank by games owned on steam. I am a moderator in the curator groups of people who rank Y,Z,U,W [anonymized, single and double digit numbers], e.t.c. Realize that there are 100 million user accounts and we are on top of it. People who collect games, people like me, we do not earn money from games, we are the ones who spend it.

Me:

> Hey! Thanks for the detailed answer: that’s why I’m writing these letters 🙂 If you are curious about my position:
1) I have no problem with collecting, but I constantly get offers through paid promotions through curator groups + key requests
for reselling from quasi-legit curators. Due to this my trust in curators has seriously been shattered, especially since multiple group have the same moderators and owners.

2) Another thing that makes me think: most of these groups have only positive reviews. I’m sure that some of the developers would like positive-only feedback for their game, but overall it creates a “fake” impression about the group.
3) Most of journalists introduce themselves. In your case I only get generic letter where the only thing has been replaced is the game name.

Reply:

> Hello Again. You know what, you are partially correct. Advertising through curators won’t give you the same effect as advertising through youtubers or magazines. On the other hand, indie-developers are glad to receive our help too. Firstly because the aforementioned youtubers and magazines are mostly concentrating on AAA games / AA indies and won’t cover it if you simply give them the copy of your $5 / $10 game. So curator advertising is actually free: for a few press copies that can’t be re-sold apart from giving it away to group moderators, you gt some exposure. For us, it’s a hobby and not a job so it’s easy to arrange a positive review.

On the other hand you need to explain what you think by “fake group.” Most of the groups have real, active subscribers. Yes, of course we bring them in through advertised free key giveaways. But nonetheless they are real players and if curator writes a review, the review is going to have some clicks. Roughly speaking, for every 10000 subscribers you’ll be getting 50-200 targeted page visits. It depends on the game: popular ones will get more, asset flips – much less. But those are real visits, which were triggered because player has seen a curator review in their timeline.

So of course don’t consider an advertisement through curators as panacea for successful sales or main marketing direction for your game, but a side thing that brings you conversions and essentially is free, for a meagre price of the game copies that can’t be passed to curator non-admins anyway. That’s why developers should actively use “Curator Connect” system, since it guarantees that no keys will be re-sold and even if curator does not make a review – the developer won’t get any financial losses.

 

____
Conclusions:

It’s up to you what you want to think about all of this. I suppose I can see it being lucrative side-business in eastern countries, mainly because of our low living expenses, so the keys can make a significant supplementary living income. Even if people ask it for their collecting hobby, they exchange your keys and because of that these keys might end up on key markets.

1) Most people who scam you out of the keys don’t see it as a theft.
2) Keys from curator connect can’t be resold. But think whether you need to send multiple keys to two curators that have essentially the same audience.
3) Scammers often register their own “real news” website domains now. Check the linked website twitter / facebook accounts. If it’s a “real” youtuber with 5k subscribers, check out their videos and if they get enough views and comments.

I’ll probably add more answers if I’ll get them, but I won’t stop sending my questions: even behind cold replies I saw the glimpses of personalities.

If you liked the article, you can follow my twitter or check out the latest game game that I’ve made with my girlfriend, called Lazy Galaxy: Rebel Story (I was able to complete my ‘research’ partly to its release 🙂

 

Great geese in flight (1925 - 1936) by Ohara Koson (1877-1945).

Great geese in flight (1925 – 1936) by Ohara Koson (1877-1945).

 

Lessons learned after two years as an indie developer

As the release of out third game, Lazy Galaxy, is coming tomorrow, I thought it would be a good time to take a break and take a look on two years of my life as indie developer.

Why/who should care? I don’t think you’ll learn something here if you develop games for 2+ years. For those who plan to start or are in the gamedev process, I hope this helps.

Short info on my background:
28 years old. Live in Riga, Latvia. Started as a PHP developer -> Worked as C++ dev for about 4 years -> quit my job and started developing games on LibGDX,for about a year, and then moved to Unity, for another year. Right now I own a gamedev studio with 2 other employees.

Should I quit my job to become indie:

A lot of people say “don’t,” especially if you don’t have any successful/published titles. I was one of the people that did that anyway. For me, the logic is simple: as you age, time becomes more important than money. If I spend 2 hours after every work day, there’s no way I’m going to be good as the guy who spends 10 hours (in case he works as effectively as I do). I sucked at gamedev two years ago and I’m not great today, but I’m doing much better. You spend time, then you improve. I started late, so if I want to be as good as others – I need to practice much more and work much harder.

My mother said that if I quit my main job, then it will be much harder to find a new one if I take a break. The thing is, your own company is not the same as taking a break, as long as you have something to show for it. If you complete projects – you are essentially working for yourself. It’s as serious as you treat it. I still get job offers from other companies, but for now I turn them down.

Basically, if you feel like you need to quit your job to do indie – quit it after you have stable professional background, possible freelance contacts and some money stashed.

The money:

I think it’s reasonable to expect that you won’t be making any money for the first year. My second year was easier (people started to notice my work and I got some projects for my company out of it). Expect hard times and have a long-term plan, together with possible short-term freelance opportunities.

The opportunity costs over those two years for me were probably potential ~$40k lost. In Latvia, you can comfortably live 3-4 years with this amount of money. We’ll see if it pays off in the future.

At first, I had to take some freelances to sustain myself further. After a while, I was able to get game-related jobs occasionally.

Health:

If you stay indoors for a long time, you’
re going to feel weak and demotivated. Fish oil helps, but try to spend more time outside. You need it.

Project Scope:

If your first game takes more than 6 months to make – reconsider. Our current game, Lazy Galaxy, took 9 months for my team to make and I can see that it’s already starting to strain and exhaust us. Our next projects will be 3-6 months tops. As you are starting out, it’s crucial to constantly get feedback. Making a game 2 years and then publishing it = in general is a too big risk for a small possible reward. I know, I know, there are exceptions and you can name them. But in general, it’s better to release four small polished games in a year than one, because you’ll be learning about game releases and getting the feedback much faster. Development is not that much of an issue (with code snippets from current asset stores and online communities you can pretty much do anything an indie wants). The biggest problem will be moving your project forward, getting noticed, marketing and building a community.

Marketing The Game:

I think that’s the most important one.

First, gamedev forums and subreddits are not really your audience. It’s easy to share there, but this probably won’t be the thing that helps you promote your game.

A side tale: in school, I was asking lots of girls about how they want to be courted by a guy. Often I got answers like “you need to compliment themor “give them gifts.Obviously, when I tried to do this, it never worked. Were they lying? Not at all, it’s just I’ve been missing an important detail: when they talked about flowers, compliments or other gifts, they were imagining that they already _like_ the person. They don’t like you because you give gifts or flowers, they like you first for their own reasons and then you give the gifts.

It’s the same when you try to promote your game: I think I’ve posted about 50 entries in screenshotsaturday over 2 years and not one of them took off. Out every game release has been followed by my own written press-release, always ignored by big press outlets.

The thing that made me think were youtubers. Lazy Galaxy did not take off on twitter, but I saw something that I did not see with my first two releases. A few (real ones, not fake accounts) 5k+ sub youtubers contacted me and asked for a copy to play. Then made videos. All by themselves. Others were more responsive than with out previous game when I messaged them.

Imagine that you’ve read one of the good posts about marketing your game. You’ve learned a lot. So you start doing it, expecting feedback. But it does not come, people mostly ignore it. You are confused, because you’ve read and done everything correctly, posted on twitter, informed the press, sent the game to influencers. When it comes to marketing – even if you do everything “by the book,” it’s not a gurantee that the game gets noticed. Aesthetics are the key. Those make your game noticed it (and then allow people to actually play it). Your game has to have style. It won’t work on a game that does not feel great (judging by the screenshots, not the actual gameplay).

In truth, you are telling people about something that they already like (but don’t know it yet), so after that they can take the steps and cover your game. If you regularly do all the classic stuff like screenshotsaturday, influencers and press with a pretty, solid, game – you will get noticed. I am 99% sure. I thought that it was mostly a matter of luck, but right now I’m just confident that you need to make a good game with visual appeal.

That translates to the next point.

Artist:

You need a style for your game. You need an artist who can work with you. All elements must feel like they belong to one style. On my first game, Blades of the Righteous, I made a mistake of commissioning arts from different artists or just buying and assets and that affected the overall feel and quality of the game. Don’t make mistakes that I did. Style matters, if you get assets – don’t get absolutely different ones from different creators. Even if they look cool separately, there’s a high chance that they won’t fit together.

At the end of my first game I was joined by my girlfriend, Helen. She became an artist and we’ve been making the games together. It has been a great journey of learning and we often feel like we’re at the beginning of our way, but we’re sure that we are moving somewhere.

During the third game, my friend Michael has agreed to work together. He’s been a valuable help due to his attention to detail and ability to thoroughly connect artistic and code sides of the project, something that I often could not do as well as him. That taught me that diversifying talent is important. He’s great because he can do something that I can’t do as effectively, and that gives me time to talk about the game, promote, translate and do QA checks.

Mentoring:

One of the things that got suggested by some articles when I started out was “find a mentor.” I’d love something like that, but I’ve emailed about 20 different experienced people from the industry to see if they can have 15-20 minute calls monthly/biweekly to mutually discuss gamedev things, but I never got a reply. At the current stage, I still need a mentor for some questions, but I don’t think I can get one without personally meeting them somewhere first.

On networking and having a gamedev business

Chances are, there are gamedev enthusiasts no matter where you live. Go to your local meetups, even if you are introverted like I am. Don’t go with some “let me be an extroverted business” guy mindset, but go to meet people with similar interests. Chances are, some of them will become friends. The conversations won’t feel forced if you are talking about the subjects that interest you both. The people will probably be more open and honest about your games too. You could also meet people that need your help, so that’s also a plus.

Make an effort. Especially if you are living in US / Western Europe. I think you have a great opportunity to talk with the people who achieved something in the industry, an opportunity that lots of us from other countries don’t have.

On the other hand, I think that (relatively) low living costs in Latvia help. I genuinely think that if you are a developer from US that’s working on established indie project – it might be worth going here to reduce your living expenses to ~$1k/month.

In Summary

I think that’s all I had in mind. I’m probably going to write a separate postmortem on Lazy Galaxy, but I wanted to make this one more personal/general advice oriented.

Overall, I see gamedev as something that I want to do for my whole life, so we’ll see if I manage to build a sustainable company in the end. Those been two tough years, but I’m optimistic about the future and actually looking forward to what happens next.

If you want to follow me: consider adding me on twitter, https://twitter.com/ColdwildGames



Future Plans

Why My Tutorial Sucks – Lessons Learned after trying to make the best tutorial

If you showcase your game on conventions – you know how important it is to hold the attention of the user and make his experience playing the game trouble-free. The way people play your game during the event actually allows you to witness how people are going to play the game at home. This opens up a path to gather real-time feedback and, of course, improve the on-boarding process for the game. You can also miss some potential fans if your game does not look appealing or understandable in the first minute.

A bit of a backstory for context: The last 4 months of my life I’ve been spending all my spare time to develop the game with a working name “Lazy Galaxy.” It’s a game where you control an evil (albeit lazy) alien race and try to conquer the universe. It’s an experimental mix between RTS and clicker/incremental game: you build a base, then you send ships to space. The ships can be controlled as in RTS games, but this is not necessary. Since I’ve never seen something like that anywhere before (let me know if you have, I want to play this game!) – there’s a challenge of introducing players to this sort of game.

This might seem like a very specific case, but I’ve learned a lot of stuff that is going to be useful for a lot of games. Over the last two months, I’ve spent a considerable development effort to ease up the introduction into the game. Here’s how it went:

Experiment 1: Text Tutorials, Smaller Convention

I’ve actually found out that I’m going to an expo only two weeks before. The game seemed complex due to worse state of the ui in early stages of development. We knew that nobody reads texts – but how bad can it actually get? We’ve drawn cute mascot portraits, made the tutorials fit the narrative and made “bouncing buttons” that highlighted where the player should be clicking. We also made UI elements appear as the game progressed, instead of throwing everything at once.

Lazy Galaxy: Old Tutorial, Walls Of Text!

Needless to say: I think only three or four people read the texts. Most people left in the first minute of the game. That’s understandable. Next time you are at a convention, try looking for an unknown games with average amounts of text explanations and play them. Are you capable of going through all instructions? I think the whole event atmosphere creates a “rush” feeling, because there are plenty of things happening around you. The event escalates the dislike for text tutorials, because no one wants to sit in one place, hindered by walls of text, unbeknownst about what are they going to get in the end.

Players also often missed our bouncing button cues, even though I’ve made all parts of the UI appear after the relevant feature becomes necessary.

Five people left their emails at the end of the day: two of them were friendly developers. Only the remaining three of these were actually people that enjoyed the game and wanted to know more.

Experiment 2: Pictures instead of text. Propaganda posters. Arrows that show where to click.

For the next convention, GameOn, biggest game convention in the baltic states, we understood that we needed a much better preparation. I’ve spent two weeks trying to figure out the best way to present information.

The first one to get added was the floating cursor helper. It showed where player needed to click, so the game would be accessible even to the players who don’t know english.

The next one to go were the text: most of the texts were thrown away. Only a simple reminder of what needs to be done (i.e. gather 20 minerals). It blinked for the first two seconds to indicate that the task has changed.

Lazy Galaxy, new tutorial: propaganda posters!

Another idea was to replace the texts with iconic images, to show how the game operates and what needs to be done. To avoid making it look like a chore, we’ve added some sort of a “propaganda” posters to the right side of instruction images. Those were designed as a references to the real-world propaganda posters. The idea was to make a game-world feel a bit more alive and add some lore. Feeling that this should do the trick, we went to the convention with high hopes.

Success?

Actually, only partially. Twenty five interested people left their emails. Not that much, but the results are much more successful than the previous ones: we genuinely found people who were interested in the game, but I’ve noticed that I often had to step-in and explain the core principles that I want to do. In convention, it might be OK, because people actually want to communicate with the developers directly and essentially you are the one “selling” you game idea. But imagine if they were playing my game at home? Nobody would be there to say “Hey, you can actually upgrade your buildings to increase an income.”

Propaganda Posets

Here are the issues:

1) While the players stayed at the game much longer, the tutorial was unable to explain the principles of the game. The players heavily relied on the arrow pointer to show what to do. They progressed through the game, but only because the game explicitly told them what to do. They could not utilize the mechanics as those were introduced: a lot of players missed the concept of building upgrades, how those increased the flow of resources and made the gathering much easier. This is not their fault, but the fault of such design. From now on I understand that arrow/clicking tips have their place to be and are effective at directing players, but should be used with caution. The trade-off: players are able to progress, but unable to learn how to use the tools the game gives them to find solutions to their problems.
2) The images with propaganda posters / instructions got closed as annoying pop-ups. There was not much difference with text popups, although they did attract some initial attention. In the end, this did not prove to be a dependable solution. The small tasks were OK though, and people often look at them.
3) Another thing that affected the first one: the tasks were telling player what actions do they need to do instead of giving them the goals they need to achieve.

Next Steps, Solutions?

Think about the following objectives and which ones sound better:
3.1) “Build a mine” or “Establish Metal supply”
3.2) “Click on 20 asteroids to gain 20 metal” or “Gain 20 metal to start building a base”
3.3) “Research Space Travel” or “Go To Space”

I’m sure you can find out the similar action vs goal-oriented tasks in your game. After witnessing the results of the first option, the second option seems like the better choice. It needs more visual cues, but this gives players the goals they need to reach instead the actions they need to take. Instead of chewing your food in their place, you give them the opportunity to taste it. Give the opportunity to explore and experience your game, instead of letting them feel constrained.

Future Plans

Here’s what I’m trying to do now: the game is going to have a special “Codex,” a dictionary with lore and it’s going to have a special chapter. There is going to be an article for each one of those instructions (that essentially contains the scheme + propaganda poster). I’m not going to show the propaganda posters right away, the “?” button will appear next to a current objective and when player click it in case he needs help.

The leading mouse pointer goes away for most cases. It is going to be replaced by element outlines / slight shaking that will appear after some time if player struggles to complete his objective.

The small objective window is going to stay. The objectives are going to be task-focused instead of action-focused. Example: instead of “Upgrade Solar Panel,” there’s going to be “Get your total energy income to 2 per second.” Essentially, allow the player to find his own solutions and feel great about this. The “feel great” part will need more work: visual cues need to be subtle, instead of “In your face, doing everything instead of you.” Hopefully, this will have better results next time.

What would you do? What do you think could work in this case?

EDIT: In case anyone is wondering, here’s how the gameplay looks (explained by yours truly)

Case Study: what I learned for Gamedev marketing after running the Game Review Blog

Some backstory: when I published my first game in 2016, nobody wanted to take a look at it; I was getting no replies from anyone. With time, I got a review on two websites (and it felt good, even though one was pretty critical). In retrospective, I can understand them now: the game was not polished enough and I had to remake it 4 times in order to make it look like something that I’m not ashamed of. I’ve recently published another game and it has been better received (and I got a few reviews on the websites too). However, at that time I’ve been quite upset about this. If no one else was there: I wanted to give anyone a chance.

I’ve started my own project in December 2016, a simple site where I started to post short game reviews, http://shortgamereviews.com. So far I have 26 reviews, different games (all on Steam) of different grades of success. After doing this for more than 2 months, I feel like I have enough I can share.

The process:

Send a personalized letter (from my domain email), offer to review the game (either by key or by review copy), if it is received – do three steps:

Some observations:

  • Bigger developers / publishers give out the review versions much easier than smaller ones. (When in fact it’s the smaller one who needs it most). That surprised me greatly.
  • To expand on that: I know how many scam emails you get asking for the keys (hey, I get them too). There are ways to identify the fake ones ( https://www.reddit.com/r/gamedev/comments/2efovt/warning_fake_game_key_requests_and_tips_to_avoid/ ). TL;DR: If someone asks you for more than 1 key – he’s probably trying to scam you.
  • Overall: Saying „No” is fine, but you need to say it. In fact, you get more respect if you answer „No” than if you keep ignoring the letter, as that helps the review planning and actually shows you are considerate of someone’s time.

Advice for developers:

  • Make your email easily available. If someone has to click multiple links to finally get an email – you are risking losing a review. If someone has to google your name to get your email – you are in for the trouble unless you’re making the next “Half-Life”.
  • If you have a separate project email – make sure to check it regularly. (Had some “two weeks later” replies).
  • You get extra points if you have some sort of preview version. I personally never did it (because well, if you the only programmer on the team – you don’t have time for everything), but I was seriously impressed by the dedication of the games that did. They also normally turned out pretty great.
  • That’s perfectly fine not to give a review key; you don’t owe it to anyone
  • When you are approached by a review website, you can request the metrics. Personally, I had no trouble sharing them (and if someone said it’s too low – that’s OK too).
  • If you get a negative review supported by arguments – it’s not assault on you personally. Replying to a review is a good chance to give your opinion, as long as you stay objective.

Some personal realizations, which I keep in mind when talking to fellow game developers:

  • Don’t be afraid of honesty. If you write that the game is good, but in truth is not – that helps no one. Developers see their sales, so the best thing you can do to your fellow devs is to tell what you think (even if their game sucks). If I dislike something about my friends’ games – I tell it. That’s the best help you can give.
  • Even bad reviews add publicity to your game. Maybe it sounds stupid, but there’s a game for everyone. If your game keeps featured somewhere – that’s a higher chance to get noticed. Either by your fans who will give counter-arguments or by people who like trashy stuff (if your game is actually bad).

 

I’ve kept the promise. I obviously don’t get as many request as big game journals do, but so far I’ve been able to look into everything that has been sent in and give a personal feedback to everyone who asked (even if it was a unity game on itch.io).

Developing Multiplatform Game with LibGDX, part 27: Android controls!

Implementing Android Controls

Technically, our game is ready, but in reality, we cannot launch it on Android. This is due to our controls read from Keyboard. Today, we are going to fix that by adding the arrow sprites for Android version.

Here’s 4 buttons that I’ve drawn:

We’re going to place 2 (up-down) buttons on the left side of the screen and remaining two on the right side. Now, let’s declare and load button images:

public TextureRegionDrawable leftArrowBtn;
public TextureRegionDrawable rightArrowBtn;
public TextureRegionDrawable upArrowBtn;
public TextureRegionDrawable downArrowBtn;

Then, at the end of Resources constructor, initialize them:

leftArrowBtn = new TextureRegionDrawable(gameSprites.findRegion("larrow"));
rightArrowBtn = new TextureRegionDrawable(gameSprites.findRegion("rarrow"));
upArrowBtn = new TextureRegionDrawable(gameSprites.findRegion("uarrow"));
downArrowBtn = new TextureRegionDrawable(gameSprites.findRegion("darrow"));

So far so good. Go to our GameScreen, declare new Group:

public Group controlGroup;

We will use it to place the button there (and also disable all of them when the game ends!). At the bottom of our GameScreen constructor, add:

controlGroup = new Group();
gameStage.addActor(controlGroup);
// comment out the next line to test buttons on desktop
if (Gdx.app.getType() == Application.ApplicationType.Android)
{
    prepareDirectionButtons();
}

We create our group as we would create any other actor. After that, we check which device launched our application. If it happened on Android – prepare button! (but if you are testing – comment out the “if” check, because you want to see the buttons on the screen during tests, but players will have no use for them).

Now, we need to define the prepareDirectionButtons function. In advance, I think all of the button functions are going to be somewhat equal (only movement direction, image and button coordinates differ). Therefore, we can make a separate function to create a direction button.

private void prepareDirectionButton(final int dx, final int dy, TextureRegionDrawable img, float x, float y)
{
    ImageButton btn = new ImageButton(img);
    btn.setPosition(x, y);
    btn.addListener(new ClickListener() {
        @Override
        public void touchUp (InputEvent event, float x, float y, int pointer, int button) {
            AttemptMove(dx, dy);
            super.touchUp(event, x, y, pointer, button);
        }
    });
    controlGroup.addActor(btn);
}

As simple as that: create a new button, create a click controller that is similar to our arrow keys (calls AttemtpMove) and then add it to our group. Now all that is left to do is to add 4 direction calls to our prepareDirectionButton:

private void prepareDirectionButtons() {
    // Up-Down
    prepareDirectionButton(0, 1, game.res.upArrowBtn, 2, gameStage.getHeight() / 2 + 2);
    prepareDirectionButton(0, -1, game.res.downArrowBtn, 2, gameStage.getHeight() / 2 - 16);

    // Left-Right
    prepareDirectionButton(-1, 0, game.res.leftArrowBtn, gameStage.getWidth() - 36, gameStage.getHeight() / 2 - 9);
    prepareDirectionButton(1, 0, game.res.rightArrowBtn, gameStage.getWidth() - 18, gameStage.getHeight() / 2 - 9);
}

After that, let’s remove the buttons once the stage is complete. At the beginning of our OnGameEnd call in GameScreen, add the following line:

public void OnGameEnd(final boolean playerWon) {
    controlGroup.remove();

Good! Now we have to make sure that the game launches in landscape mode on Android. Go to our AndroidManifest.xml file and adjust it accordingly (if you have not already):

android:screenOrientation="landscape"

That’s it! The game is going to have the direction buttons now. Try running it on your phone to see if everything’s working as intended.

Multiplatform Gamedev Tutorial

Relevant git commit: https://github.com/vladimirslav/dodginghero/commit/183f5b02b8ddeff08ad71c7681e1ef766ecb50c1

Developing Multiplatform Game with LibGDX, part 26: Music!

Lesson 26 – Background Music

So in the previous lesson, we implemented the game sounds. But there’s no background music and no way to control it (turn it off / make it quieter).

Playing Music is actually pretty simple. Here’s what I added to SoundManager class:

public static Music bMusic = null;
public static void StopBattleMusic()
{
    if (bMusic != null)
    {
        bMusic.stop();
        bMusic = null;
    }
}

public static void PlayBattleMusic()
{
    bMusic = Gdx.audio.newMusic(Gdx.files.internal("music/music" + MathUtils.random(5) + ".mp3"));
    bMusic.setLooping(true);
    bMusic.play();
}

Music does not really work like sounds. We preload sounds, but music is different: since music files can be quite big, they are simply being read in realtime. We’ll simply pick the random tune from music0…music6, set it to looping and then play it. Now, just add a call to start music at the initialization of GameScreen and Stop the music at the disposal.

…
public GameScreen(DodgingHero _game) {
    super(_game);
    batch = new SpriteBatch();
    bg = new Background();
    SoundManager.PlayBattleMusic();
…
@Override
public void dispose()
{
    SoundManager.StopBattleMusic();
    super.dispose();
…

Alright. This part is done. Now let’s make sure we have a button to control the sound. I have four textures with sound button, to indicate different sound/music volume. 0%, 33%, 66%, 100%, called sound0, sound1, sound2 and sound3 respectively.

Adjust our Resources class by adding TextureRegionDrawable array called soundBtn;

public TextureRegionDrawable soundBtn[];

Then, load them:

soundBtn = new TextureRegionDrawable[4];
for (int i = 0; i < soundBtn.length; i++)
{
    soundBtn[i] = new TextureRegionDrawable(gameSprites.findRegion("sound" + i));
}

Now, let’s make a setting for the sound. We could make a Settings file similar to GameProgress file, but since we only have one setting (sound), let’s implement it in GameProgress. If you plan on adding more – I strongly suggest separating into your own settings file.

In our GameProgress file, add a constant to indicate MAX_SOUND_VALUE (that would be 3, 0..3), then add a new static variable called soundVolume; Also, add a save key for it.

public static final int MAX_SOUND_VOLUME = 3;
public static int soundVolume = MAX_SOUND_VOLUME;
private static final String SAVE_KEY_SOUND_VOLUME = "soundvolume";

Make sure to save/load them. In Load():

soundVolume = prefs.getInteger(SAVE_KEY_SOUND_VOLUME, MAX_SOUND_VOLUME);

In Save:

prefs.putInteger(SAVE_KEY_SOUND_VOLUME, soundVolume);

And add a new static function ToggleVolume();

public static void ToggleVolume() {
    soundVolume += 1;
    if (soundVolume > MAX_SOUND_VOLUME)
    {
        soundVolume = 0;
    }
}

Adjust the volume, if it goes over maximum value, just start anew (by disabling it, setting it to 0). Finally, let’s integrate the change to our SoundManager. When we play music/sound, we need to take the value of the soundVolume into account. First, the volume:

private static void playSoundRandomVolume(Sound sound, float min, float max)
{
    if (sound != null)
    {
        sound.play(MathUtils.random(min, max) * GameProgress.soundVolume / GameProgress.MAX_SOUND_VOLUME);
    }
}

Just multiply randomly generated value with soundVolume. Do something similar for music.

public static void PlayBattleMusic()
{
    bMusic = Gdx.audio.newMusic(Gdx.files.internal("music/music" + MathUtils.random(5) + ".mp3"));
    bMusic.setLooping(true);
    bMusic.setVolume((float)GameProgress.soundVolume / GameProgress.MAX_SOUND_VOLUME);
    bMusic.play();
}

Casting to float is important! Otherwise the division happens between two integer values and you end up dividing 1 with 3 and get zero as a result. So you have to be careful with things like these. Taking the current music volume is great. But what if we change the volume during the game? We’ll need to modify the current music volume. Create a new static function, called AdjustVolume. It will call GameProgress.ToggleVolume and then change the volume of the currently playing music to a new value.

public static void AdjustVolume()
{
    GameProgress.ToggleVolume();
    if (bMusic != null)
    {
        bMusic.setVolume((float)GameProgress.soundVolume / GameProgress.MAX_SOUND_VOLUME);
    }
}

That part is done! Now let’s make the sound button. In our GameScreen, add a new variable called

ImageButton sndBtn;

Then initialize it in our GameScreen constructor:

sndBtn = new ImageButton(game.res.soundBtn[GameProgress.soundVolume]);
sndBtn.setPosition(gameStage.getWidth() - sndBtn.getWidth() - 10, 10);
sndBtn.addListener(new ClickListener() {
    public void touchUp (InputEvent event, float x, float y, int pointer, int button) {
        SoundManager.AdjustVolume();
        sndBtn.getStyle().imageUp = game.res.soundBtn[GameProgress.soundVolume];
        super.touchUp(event, x, y, pointer, button);
    }
});

gameStage.addActor(sndBtn);

The idea is simple: change the image after the button has been pressed, indicating the current state of the volume.

Now, if you run the game, you’ll notice that the button is there but it is not clickable. The reason is that we set up our input to GameScreen and not gameStage. We catch events in our gamescreen, but we should be doing it in our stage. Thus, we have to move our keydown function into gamestage. First, remove the InputProcessor implementation from the GameScreen and move keyDown function to stage. Make sure to change the parameters from int keyCode to InputEvent event, int keyCode.

Gdx.input.setInputProcessor(gameStage);
gameStage.addListener(new InputListener(){
      @Override
      public boolean keyDown(InputEvent event, int keyCode) {
          switch (keyCode)
          {
              case Input.Keys.RIGHT:
                  AttemptMove(1, 0);
                  break;
              case Input.Keys.LEFT:
                  AttemptMove(-1, 0);
                  break;
              case Input.Keys.UP:
                  AttemptMove(0, 1);
                  break;
              case Input.Keys.DOWN:
                  AttemptMove(0, -1);
                  break;
              default:
                  break;
          }

          return false;
      }

});

Now, try running the game and pressing the button. You’ll see that both sound/music are adjusted! Awesome! We have a sound button and we can adjust the game volume now!

Disclaimer: I’ve also tampered with game ending functions (not describing them here) to change transitions in GameScreen for html version because it was bugged otherwise. I don’t think that’s relevant to the lesson, but you can check the git commit with full code here (look for endgame boolean):

https://github.com/vladimirslav/dodginghero/commit/223be4e0ecd6518608e0207196b28642b755f8e0

This concludes our second part of lessons, where we polished the game. Next, I’m going to focus on the Android-specific stuff implementation: ads, in-app purchases.

If you want to expand the game idea – think how you can make a special abilities for every character or add more bonuses or stats (gold per bonus picked?). The effects can also be adjusted, floating numbers on damage or when player gains gold. Never stop thinking about the things that can be improved!

Play the game online: http://coldwild.com/dodge2/

Developing Multiplatform Game with LibGDX, part 25: SOUND! And bug fixes.

Minor tweaks

The gameplay is mostly done. One major flaw that remains is the lack of „restart”/”character selection” buttons after the game is over.

Add

eventListener.OnGameEnd(false);

function call in our GameLogic class, exactly once player’s hp reaches zero, right before GameProgress.Reset(true); call. Then modify our GameEventListener and make playerWon Boolean final.


public interface GameEventListener
{
    void OnGameEnd(final boolean playerWon);
}

Modify GameScreen’s OnGameEnd listener function to actually make use of playerWon variable. On our last action (where we get rid of the current screen. Here’s how it looks now:

new Action() {
    @Override
    public boolean act(float delta) {
        dispose();
        if (playerWon)
        {
            game.setScreen(new GameScreen(game));
        }
        else
        {
            game.setScreen(new CharacterSelectionScreen(game));
        }
        return true;
    }
}

After player gets defeated, he gets brought to CharacterSelectionScreen. That’s it. We no longer need to relaunch the game file after loss.

Few bugs:

  • Let’s Tweak our “Start” button and call GameProgress.Reset(false) before the game starts: this will ensure that player gets his max hp (in case we made an upgrade and it raised the hp).
  • After the player wins the stage – make sure to record his hp to currentLives. In our Player.markVictorious function, save the current player’s hp to GameProgress.playerLives:
public void markVictorious()
{
    winTime = timeAlive;
    winning = true;
    GameProgress.playerLives = lives;
}

Sound and Music introduction

Our final (and very important!) part is going to be about music. I’ve got two music/sound packs from opengameart.com:

Converted them to .ogg (smaller size) with http://audio.online-convert.com/convert-to-ogg tool.

I’ve converted the following files from attack sounds:

  • swing.wav -> swing0.ogg
  • swing2.wav -> swing1.ogg
  • swing3.wav -> swing2.ogg
  • coin.wav -> coin.ogg
  • bite-small.wav -> heal.ogg
  • fantozzi-sandl1 -> walk0.ogg
  • fantozzi-sandl2 -> walk1.ogg
  • Fantozzi-SandR1 -> walk2.ogg

Music is left-as is, in mp3, but renamed to music0..music5.ogg. In the directory android/assets/, create music folder and put sounds/music there. Finally, let’s get to coding! In our com.coldwild.dodginghero package, create a new class and call it SoundManager. It’s going to have static functions that controls menu music. Let’s introduce functions that load and unload sounds.

public class SoundManager {

    public static AssetManager assets = new AssetManager();

    public static void LoadSounds() {
        for (int i = 0; i < 3; i++)
        {
            assets.load(Gdx.files.internal("music/swing" + i + ".ogg").path(), Sound.class);
            assets.load(Gdx.files.internal("music/walk" + i + ".ogg").path(), Sound.class);
        }


        assets.load("music/coin.ogg", Sound.class);
        assets.load("music/heal.ogg", Sound.class);
        assets.finishLoading();
    }

    public static void ReleaseSounds()
    {
        assets.dispose();
    }


}

In our DodgingHero class, in our create function, after GameProgress.Load() call, add the call to LoadSounds function:

SoundManager.LoadSounds();

And

SoundManager.ReleaseSounds();

At the end of dispose() function. This will ensure the sounds are loaded at the start of the game and disposed at the end. The easiest ones to implement would be walk / attack sounds. Add those things to our SoundManager:

private static void playSoundRandomVolume(Sound sound, float min, float max)
{
    if (sound != null)
    {
        sound.play(MathUtils.random(min, max));
    }
}

public static void PlaySwingSound()
{
    Sound s = assets.get("music/swing" + MathUtils.random(2) + ".ogg", Sound.class);
    playSoundRandomVolume(s, 0.90f, 1.0f);
}

public static void PlayWalkSound()
{
    Sound s = assets.get("music/walk" + MathUtils.random(2) + ".ogg", Sound.class);
    playSoundRandomVolume(s, 0.45f, 0.55f);
}

Pretty straightforward: pick the necessary (random!) sound, then play it at a reasonably random volume to make it even more randomized. Put the calls to the following functions:

Character.takeDamage()

public void takeDamage(int amnt)
{
    SoundManager.PlaySwingSound();
    timeOfDmgTaken = timeAlive;
    lives -= amnt;
    if (lives < 0)
    {
        lives = 0;
    }
}

And in our GameScreen.AttemptMove

public void AttemptMove(int dx, int dy)
{
    if (player.getLives() > 0 &&
        enemy.getLives() > 0 &&
            logic.CheckMove(player.getFieldX() + dx, player.getFieldY() + dy))
    {
        logic.AssignPlayerPosition(player.getFieldX() + dx, player.getFieldY() + dy);
        SoundManager.PlayWalkSound();
    }
}

Try running the game! Whenever you move or your player gets hit – you’ll hear it. As you remember, we try to split game logic from representation. We won’t be calling soundmanager from our gamelogic, we’ll pass the bonus pickup / attack even to our gamescreen and it will handle it properly. First, add the functions for coin / health pickup:

public static void PlayCoinSound()
{
    Sound coin = assets.get("music/coin.ogg", Sound.class);
    playSoundRandomVolume(coin, 0.9f, 1.0f);
}

public static void PlayHealSound()
{
    Sound heal = assets.get("music/heal.ogg", Sound.class);
    playSoundRandomVolume(heal, 0.9f, 1.0f);
}

In our GameEventListener interface, add a new function declaration:

void OnBonusPickup(byte bonusType);

We’ll call it when player picks a bonus, in our AssignPlayerPosition, right after the check:

if (currentBonus.getFieldX() == fx &&
        currentBonus.getFieldY() == fy)
{
    eventListener.OnBonusPickup(currentBonus.getBonusType());

The only thing left for GameScreen is to implement OnBonusPickup method.

@Override
public void OnBonusPickup(byte bonusType) {
    if (bonusType == Bonus.BONUS_TYPE_COIN)
    {
        SoundManager.PlayCoinSound();
    }
    else if (bonusType == Bonus.BONUS_TYPE_HEALTH)
    {
        SoundManager.PlayHealSound();
    }
}

Run the game, hear more sounds! Awesome. That concludes this lesson. In our next lesson, we’ll work on adding music.

Relevant git commit: https://github.com/vladimirslav/dodginghero/commit/e93739a981ad6da90c2fe69bfce876ae1be045c9

Developing Multiplatform Game with LibGDX, part 24: improving progression and rewards

game progress balancing, replayability

Our stats affect the game, but what happens if you unlock low-level character after you’ve cleared lots of game stages with your high-level one? The difficulty simply won’t be up to par. We have to keep current stage numbers separately for each character.

In our GameProgress class, get rid of currentLevel variable (and also let’s make the naming less confusing). Also, get rid of SAVE_KEY_CURRENT_LEVEL constant. The game levels will be called „game stages” in order to avoid confusion. Add a new save key, called

private static final String SAVE_KEY_PLAYER_STAGE = "playerstage";

Right beside our levels variable, create a new array, called „stages”; Initialize it right beside our levels. Adjust the Load function to set stages to zero by default. Here’s how my Load function looks like now:

public static void Load()
{
    levels = new int[CharacterRecord.CHARACTERS.length];
    stages = new int[CharacterRecord.CHARACTERS.length];

    Preferences prefs = Gdx.app.getPreferences(PROGRESS_SAVE_NAME);

    for (int i = 0; i < CharacterRecord.CHARACTERS.length; i++)
    {
        levels[i] = prefs.getInteger(SAVE_KEY_PLAYER_LEVEL + i, i == 0 ? 1 : 0);
        stages[i] = prefs.getInteger(SAVE_KEY_PLAYER_STAGE + i, 0);
    }
//...

Don’t forget about Save function:

for (int i = 0; i < CharacterRecord.CHARACTERS.length; i++)
{
    prefs.putInteger(SAVE_KEY_PLAYER_LEVEL + i, levels[i]);
    prefs.putInteger(SAVE_KEY_PLAYER_STAGE + i, stages[i]);
}

Now, let’s get rid of errors which we inevitably got after removing currentLevel variable.

public static int getEnemyLives()
{
    return 3 + currentLevel * 2;
}

Becomes

public static int getEnemyLives()
{
    return 3 + stages[currentCharacter] * 2;
}

Let’s also add getEnemyDamage function (it makes sense to increase the damage as player’s hp increases too, right?)

public static int getEnemyDamage()
{
    return 1 + stages[currentCharacter] / 10; // increase damage every 10 stages
}

The Reset function needs to be heavily adjusted.

public static void Reset() {
    currentLevel = 0;
}

 

Forget about it. After player dies, let’s simply reduce his current stage by 5 (so he does not have to do everything from the beginning). We also need to reset player’s hp. Important note: when a new character is chosen in CharacterSelectionScreen, we also must reset player’s hp (since each char can have separate max hp now). Let’s introduce extra Boolean parameter to Reset, called resetProgress. It is only going to be true when player dies, and not simply changes the character.

public static void Reset(boolean resetProgress) {
    if (resetProgress)
    {
        stages[currentCharacter] -= 5;
        if (stages[currentCharacter] < 0)
        {
            stages[currentCharacter] = 0; // don't let it go below zero!
        }
    }

    playerLives = getPlayerMaxHp();
}

Now, let’s arrange the new reset calls. Go to CharacterSelectionScreen and alter the code of our next and prev buttons.

nextBtn.addListener(new ClickListener() {
    public void touchUp (InputEvent event, float x, float y, int pointer, int button) {
        GameProgress.currentCharacter += 1;
        if (GameProgress.currentCharacter == CharacterRecord.CHARACTERS.length)
        {
            GameProgress.currentCharacter = 0;
        }
        GameProgress.Reset(false);
        prepareUi();
    }
});
…
prevBtn.addListener(new ClickListener() {
    public void touchUp (InputEvent event, float x, float y, int pointer, int button) {
        GameProgress.currentCharacter -= 1;
        if (GameProgress.currentCharacter < 0)
        {
            GameProgress.currentCharacter = CharacterRecord.CHARACTERS.length - 1;
        }
        GameProgress.Reset(false);
        prepareUi();
    }
});

And fix the GameLogic a bit: firstly, we cannot simply increase current level when the enemy has less than 0 lives (we removed the variable, remember?). Let’s make a separate function inside the GameProgress, named increaseStage(). Let’s also grant player some money on level completion. Replace:

GameProgress.currentLevel++; 

With

GameProgress.increaseStage(); 

increaseStage implementation inside our GameProgress:

public static void increaseStage() {
    currentGold += 1 + stages[currentCharacter] / 4; // increase gold gain every 4 levels
    stages[currentCharacter]++;
}

The values are taken somewhat randomly, so only testing will show if those values are good. Always be on lookout to see what’s going on with the game balance. Two last changes:

player.takeDamage(1);
if (player.getLives() <= 0)
{
    GameProgress.Reset();
}

Pass true to our Reset function (we want to reduce levels when player has lost the game) and also use the new enemy damage vs player.


player.takeDamage(GameProgress.getEnemyDamage());
if (player.getLives() <= 0)
{
    GameProgress.Reset(true);
}

That should do it. Not much to see, but the gameplay-wise this will change a lot.

Relevant git commit: https://github.com/vladimirslav/dodginghero/commit/49e8b0fe0fa156e17acd6d4ab01d798296f70f03

Developing Multiplatform Game with LibGDX, part 23: stat logic and display

Assigning meaning to stats

So far in our game, we can upgrade the character and unlock new ones. But the upgrades do nothing. Let’s actually make them affect the game.

Player’s hp, hp regen, damage from attack and bonus spawn time is going to depend on character level. Go to our CharacterRecord class. Introduce four functions to get each value. We are going to pass the level of the character, the function is going to return the value.

For HP, damage and regen – let’s make linear functions. First, get rid of maxPlayerLives and playerDamage in GameProgress. While you’re there, also get rid of playerLives = 3 in our „Reset” function. Then, go back to CharacterRecord and implement the three functions.

public int getMaxHp(int level)
{
    return 3 + level / levelsForHpUpgrade;
}

public int getDmg(int level)
{
    return 1 + level / levelsForAttackUpgrade;
}

public int getHpRestored(int level)
{
    return 1 + level / levelsForHpRegenUpgrade;
}

Pretty straightforward. Take the base value, add level divided by the levels necessary for upgrade, get result. Looks good. What about bonus spawning? We don’t want the bonus time to go down drastically, so it makes sense to introduce diminishing returns.

public float getBonusSpawnReduction(int level)
{
    int bonusSpawnLvl = level / levelsForBonusSpawnUpgrade;
    return bonusSpawnLvl / (30 + bonusSpawnLvl); // 30 enables diminishing returns, x / ( x + 30)
}

30 is just a magical number. You can make it any value you want. All we have to do is put it into necessary places now.

Go to our Player class. Change the lines

max_lives = GameProgress.maxPlayerLives;
set(res.playerSprites.get(CharacterRecord.CHARACTERS[GameProgress.currentCharacter].name));

To

max_lives = GameProgress.getPlayerMaxHp();
set(res.playerSprites.get(CharacterRecord.CHARACTERS[GameProgress.currentCharacter].name));

In our GameProgress class, define the following function:

public static int getPlayerMaxHp() {
    CharacterRecord currentChar = CharacterRecord.CHARACTERS[currentCharacter];
    return currentChar.getMaxHp(levels[currentCharacter]);
}

We just lookup the current character record, then try to get max hp depending on the level (we’ve also made the sprite setting line a bit more readable. Now, let’s modify GameLogic for our new character stats.

First, go to our AssignPlayerPosition function. There are two places that we should change. First,

else if (currentBonus.getBonusType() == Bonus.BONUS_TYPE_ATTACK)
{
    enemy.takeDamage(GameProgress.playerDamage);

Let’s replace GameProgress.playerDamage with static function of the similar name.

else if (currentBonus.getBonusType() == Bonus.BONUS_TYPE_ATTACK)
{
    enemy.takeDamage(GameProgress.getPlayerDamage());

In our GameProgress, create a function getPlayerDamage():

public static int getPlayerDamage() {
    CharacterRecord currentChar = CharacterRecord.CHARACTERS[currentCharacter];
    return currentChar.getDmg(levels[currentCharacter]);
}

While you’re still there, do the same for hp restored per bonus:

public static int getPlayerHealthRestored() {
    CharacterRecord currentChar = CharacterRecord.CHARACTERS[currentCharacter];
    return currentChar.getHpRestored(levels[currentCharacter]);
}

Go back to logic. In BONUS_TYPE_HEALTH check, replace player.addLives(1)  with

player.addLives(GameProgress.getPlayerHealthRestored());

Now, the only thing left to modify is our BONUS_SPAWN_INTERVAL. We’re going to set it’s value during GameLogic construction (since player cannot levelup during the battles).

Remove the default value of 2.0f from BONUS_SPAWN_INTERVAL and remove keyword static.

private final float BONUS_SPAWN_INTERVAL;

Instead, initialize BONUS_SPAWN_INTERVAL in GameLogic constructor.

BONUS_SPAWN_INTERVAL = 2.0f * (1 - GameProgress.getPlayerBonusReduction());

The function itself:

public static float getPlayerBonusReduction() {
    CharacterRecord currentChar = CharacterRecord.CHARACTERS[currentCharacter];
    return currentChar.getBonusSpawnReduction(levels[currentCharacter]);
}

 

Displaying Stat Information

 

That’s it. The changes should affect the game now. But we cannot really see them in character selection scree. Let’s fix that! First, move the hero sprite to the left by adjusting its X position:

heroSprite.setPosition((uiStage.getWidth() - heroSprite.getWidth()) / 4,
                       (uiStage.getHeight() - heroSprite.getHeight()) / 2);

Then, move the arrow button to the top of the screen (otherwise they just start getting in the way).

prevBtn.setPosition(uiStage.getWidth() / 6 - prevBtn.getWidth() / 2, uiStage.getHeight() * 5 / 6);
//...
nextBtn.setPosition(uiStage.getWidth() * 5 / 6 - nextBtn.getWidth() / 2,
        uiStage.getHeight() * 5 / 6);

Then, let’s actually work on adding the labels with stats. Since we have 4 stats which will involve repetitive label creation, make a function to do that:

private Label prepareStatLabel(String text, float x, float y, Label.LabelStyle textStyle)
{
    Label lbl = new Label(text, textStyle);
    lbl.setAlignment(Align.left);
    lbl.setPosition(x, y);
    uiStage.addActor(lbl);
    return lbl;
}

It will return the newly created label, so that it can be used to properly figure out the coordinates of the next one below it. I add the stats after I add heroSprite to uiStage:

uiStage.addActor(heroSprite);

Label stat = prepareStatLabel("DMG:" + GameProgress.getPlayerDamage(),
        uiStage.getWidth() / 2,
        heroSprite.getY() + heroSprite.getHeight(),
        textStyle);

stat = prepareStatLabel("HP:" + GameProgress.getPlayerMaxHp(),
        uiStage.getWidth() / 2,
        stat.getY() - 10,
        textStyle);

stat = prepareStatLabel("HEAL:" + GameProgress.getPlayerHealthRestored(),
        uiStage.getWidth() / 2,
        stat.getY() - 10,
        textStyle);

prepareStatLabel("BNS:" + GameProgress.getBonusReductionValue(),
        uiStage.getWidth() / 2,
        stat.getY() - 10,
        textStyle);
;

//Bonus function for reference in GameProgress:
public static int getBonusReductionValue() {
    CharacterRecord currentChar = CharacterRecord.CHARACTERS[currentCharacter];
    return levels[currentCharacter] / currentChar.levelsForBonusSpawnUpgrade;
}

This should do it! Not only stats are working, but they are also displayed properly. Here’s what I have now:

Relevant git commit: https://github.com/vladimirslav/dodginghero/commit/c867d7ca47e0ea721353d03107ee7f9e160d32aa

Developing Multiplatform Game with LibGDX, part 22: unlocking characters / levelling up

Lesson 22: Ways to Spend Gold: Unlocking / Upgrading characters

So in the previous lesson we’ve implemented a way to collect gold, an in-game resource. But the trouble is: there’s no way to spend it yet. Let’s fix it today!

I am planning to do two things today: make characters unlockable and enable „upgrade” button for unlocked characters. All characters (except for human) are going to be locked by default. As soon as player collects 1000 gold – he can unlock a character. The amount of gold is up to you. I think 500 is a good amount, because we’ll be able to introduce In-App-Purchases later which would grant enough gold, but also player can gather enough once he played some time (without paying a dime).

Alright, so let’s modify our progress file first. We’re going to adjust it gradually, according to the changes what we are making. First thing that we are going to do, let’s make a separate array of levels of each character. 0 will mean that the character is locked, any value above that will simply mean character level (1+). Introduce the following constants / variables:


public static final int CHARACTER_PRICE = 1000; // price to unlock a character

public static int levels[]; // level of each character, 0 = locked

private static final String SAVE_KEY_PLAYER_LEVEL = "playerlevel";

We’re going to init the array in our Load function. Since we are saving/loading multiple values, we’re simply going to add an index to the key.

public static void Load()
{
    levels = new int[CharacterRecord.CHARACTERS.length];

    Preferences prefs = Gdx.app.getPreferences(PROGRESS_SAVE_NAME);

    for (int i = 0; i < CharacterRecord.CHARACTERS.length; i++)
    {
        levels[i] = prefs.getInteger(SAVE_KEY_PLAYER_LEVEL + i, i == 0 ? 1 : 0);
    }

Something similar goes to saving. In our Save() function, add the following code:

for (int i = 0; i < CharacterRecord.CHARACTERS.length; i++)
{
    prefs.putInteger(SAVE_KEY_PLAYER_LEVEL + i, levels[i]);
}

This should do it for saving/loading. Now that the backend is (somewhat) handled – let’s adjust the menu to allow the magic to happen!

So, first, let’s modify our “Start” button to be replaced with “Unlock” button if character is locked. The code is pretty simple:

if (GameProgress.levels[GameProgress.currentCharacter] == 0)
{
    TextButton startBtn = new TextButton("Unlock(1000 Gold)", buttonStyle);
    startBtn.setPosition((uiStage.getWidth() - startBtn.getWidth()) / 2, uiStage.getHeight() / 6);
    startBtn.addListener(new ClickListener() {
        public void touchUp (InputEvent event, float x, float y, int pointer, int button) {
            if (GameProgress.currentGold >= GameProgress.CHARACTER_PRICE)
            {
                GameProgress.currentGold -= GameProgress.CHARACTER_PRICE;
                GameProgress.levels[GameProgress.currentCharacter] = 1;
                prepareUi();
            }
        }
    });
    uiStage.addActor(startBtn);
}
else
{
    // start / upgrade button code goes here
    TextButton startBtn = new TextButton("START", buttonStyle);
    startBtn.setPosition((uiStage.getWidth() - startBtn.getWidth()) / 2, uiStage.getHeight() / 6);
    startBtn.addListener(new ClickListener() {
        public void touchUp (InputEvent event, float x, float y, int pointer, int button) {
            dispose();
            game.setScreen(new GameScreen(game));
        }
    });
    uiStage.addActor(startBtn);
}

We just add a different button in case the character is locked.

You might have noticed that we have to call uiStage.clear() before every prepareUi call. So, this is redundant. Let’s simply move uiStage.clear() to the beginning of prepareUi() and remove all other uiStage.clear() calls on this screen.

Run the game and try switching characters. If you set the CHARACTER_PRICE to lower value for debugging purposes and try unlocking a character – you’ll see that it becomes available.

After that, let’s get to upgrade button. But first, I think it makes sense if we add a label with character level or just a warning (“char locked!”). First, move textStyle declaration/initialization on top of our prepareUi function, right above the buttonStyle declaration.

Now, let’s make a new label, after our heroSprite definition. (Because we’re going to use that as an orientation / position settings).

uiStage.addActor(heroSprite);

// char level
int lvl = GameProgress.levels[GameProgress.currentCharacter];
Label statusText = new Label(lvl > 0 ? "LVL: " + lvl : "LOCKED", textStyle);
statusText.setPosition(heroSprite.getX() + (heroSprite.getWidth() - statusText.getWidth()) / 2,
        heroSprite.getY() - statusText.getHeight() - 5);
uiStage.addActor(statusText);

Now we can finally get to upgrade button. You might have noticed, that the interface is getting a bit overcrowded. Let’s move the start button to the top and levelup button to the bottom (where there’s Unlock button for locked chars).

Here’s how the code looks like:

else
{
    // start / upgrade button code goes here
    TextButton startBtn = new TextButton("START", buttonStyle);
    startBtn.setPosition((uiStage.getWidth() - startBtn.getWidth()) / 2, uiStage.getHeight() * 5 / 6);
    startBtn.addListener(new ClickListener() {
        public void touchUp (InputEvent event, float x, float y, int pointer, int button) {
            dispose();
            game.setScreen(new GameScreen(game));
        }
    });
    uiStage.addActor(startBtn);


    TextButton upgradeBtn = new TextButton(
            "LvlUp(" + GameProgress.getNextUpgradeCost(GameProgress.currentCharacter) + ")",
            buttonStyle);
    upgradeBtn.setPosition((uiStage.getWidth() - upgradeBtn.getWidth()) / 2, uiStage.getHeight() / 6);
    upgradeBtn.addListener(new ClickListener() {
        public void touchUp (InputEvent event, float x, float y, int pointer, int button) {
            if (GameProgress.currentGold >= GameProgress.getNextUpgradeCost(GameProgress.currentCharacter))
            {
                GameProgress.currentGold -= GameProgress.getNextUpgradeCost(GameProgress.currentCharacter);
                GameProgress.levels[GameProgress.currentCharacter] += 1;
                prepareUi();
            }
        }
    });
    uiStage.addActor(upgradeBtn);
}

The start button code pretty much remains unchanged, except for Y position, which has moved to top of the screen.

UpgradeBtn is similar to start button, but it invokes getNextUpgradeCost function (will get to it later). The position is the same as the old “start” button position. The click function acts in a similar way, checks the next level upgrade cost and if it’s fine – takes the money and increases the character level. Looks good? Let’s go to GameProgress and define the function to get next level cost. As an experiment, I think it’s fine if we multiply the current level value by 2.

public static int getNextUpgradeCost(int currentCharacter) {
    return levels[currentCharacter] * 2;
}

I’ve also noticed a bug, CharacterSelectionScreen does not have resize handling, so resizing gets ugly. Let’s fix that!

@Override
public void resize(int width, int height)
{
    super.resize(width, height);
    uiStage.getViewport().update(width, height, true);
}

Run the game! If you have some gold already – upgrade your character. For now, the upgrade is only visual, but don’t get upset – we’re going to fix this in the next lesson! See you then.

Git commit: https://github.com/vladimirslav/dodginghero/commit/564990aeed19a887e568666bf85d9b52865c7466