mirror of
https://github.com/godotengine/godot-docs.git
synced 2026-01-04 14:11:02 +03:00
Improve docs structure
Change Step by Step in Getting Started Remove Engine Features category, move into a new Tutorials top-level section
This commit is contained in:
130
tutorials/platform/android_in_app_purchases.rst
Normal file
130
tutorials/platform/android_in_app_purchases.rst
Normal file
@@ -0,0 +1,130 @@
|
||||
.. _doc_android_in_app_purchases:
|
||||
|
||||
Android in-app purchases
|
||||
========================
|
||||
|
||||
.. highlight:: shell
|
||||
|
||||
Godot engine has integrated GooglePaymentsV3 module with which we can implement in-app purchases in our game.
|
||||
|
||||
The Godot engine demo project repository has an android-iap example project. It includes a gdscript interface for android IAP.
|
||||
|
||||
Check the repository here https://github.com/godotengine/godot-demo-projects
|
||||
|
||||
Find the iap.gd script in
|
||||
|
||||
::
|
||||
|
||||
godot-demo-projects/misc/android_iap
|
||||
|
||||
|
||||
Add it to the Autoload list and name it as IAP so that we can reference it anywhere in the game.
|
||||
|
||||
Getting the product details
|
||||
---------------------------
|
||||
|
||||
When starting our game, we will need to get the item details from Google such as the product price, description and localized price string etc.
|
||||
|
||||
::
|
||||
|
||||
#First listen to the sku details update callback
|
||||
IAP.connect("sku_details_complete",self,"sku_details_complete")
|
||||
|
||||
#Then ask google the details for these items
|
||||
IAP.sku_details_query(["pid1","pid2"]) #pid1 and pid2 are our product ids entered in Googleplay dashboard
|
||||
|
||||
|
||||
#This will be called when sku details are retrieved successfully
|
||||
func sku_details_complete():
|
||||
print(IAP.sku_details) #This will print the details as JSON format, refer the format in iap.gd
|
||||
print(IAP.sku_details["pid1"].price) #print formatted localized price
|
||||
|
||||
We can use the IAP details to display the title, price and/or description on our shop scene.
|
||||
|
||||
Check if user purchased an item
|
||||
-------------------------------
|
||||
|
||||
When starting our game, we can check if the user has purchased any product. YOU SHOULD DO THIS ONLY AFTER 2/3 SECONDS AFTER YOUR GAME IS LOADED. If we do this as the first thing when the game is launched, IAP might not be initialized and our game will crash on start.
|
||||
|
||||
::
|
||||
|
||||
#Add a listener first
|
||||
IAP.connect("has_purchased",self,"iap_has_purchased")
|
||||
IAP.request_purchased() #Ask Google for all purchased items
|
||||
|
||||
#This will call for each and every user purchased products
|
||||
func iap_has_purchased(item_name):
|
||||
print(item_name) #print the name of purchased items
|
||||
|
||||
|
||||
Google IAP policy says the game should restore the user's purchases if the user replaces their phone or reinstalls the same app. We can use the above code to check what products the user has purchased and we can make our game respond accordingly.
|
||||
|
||||
Simple Purchase
|
||||
---------------
|
||||
|
||||
We can put this purchase logic on a product's buy button.
|
||||
|
||||
::
|
||||
|
||||
#First listen for purchase_success callback
|
||||
IAP.connect("purchase_success",self,"purchase_success_callback")
|
||||
|
||||
#Then call purchase like this
|
||||
IAP.purchase("pid1") #replace pid1 with your product id
|
||||
IAP.purchase("pid2") #replace pid2 with your another product id
|
||||
|
||||
#This function will be called when the purchase is a success
|
||||
func purchase_success_callback(item):
|
||||
print(item + " has purchased")
|
||||
|
||||
We can also implement other signals for the purchase flow and improve the user experience as you needed.
|
||||
|
||||
``purchase_fail`` - When the purchase is failed due to any reason
|
||||
|
||||
``purchase_cancel`` - When the user cancels the purchase
|
||||
|
||||
``purchase_owned`` - When the user already bought the product earlier
|
||||
|
||||
|
||||
Consumables and Non-Consumables
|
||||
-------------------------------
|
||||
|
||||
There are two types of products - consumables and non-consumables.
|
||||
**Consumables** are purchased and used, for eg: healing potions which can be purchased again and again.
|
||||
**Non-consumables** are one time purchases, for eg: Level packs.
|
||||
|
||||
Google doesn't have this separation in their dashboard. If our product is a consumable, and if a user has purchased it, it will not be available for purchase until it is consumed. So we should call the consume method for our consumables and don't call consume for your non-consumables.
|
||||
|
||||
::
|
||||
|
||||
IAP.connnect("consume_success",self,"on_consume_success")
|
||||
IAP.consume("pid")
|
||||
|
||||
func on_consume_success(item):
|
||||
print(item + " consumed")
|
||||
|
||||
If our game has only consumables, we don't have to do this. We can set it to consume the item automatically after a purchase.
|
||||
|
||||
::
|
||||
|
||||
IAP.set_auto_consume(true)
|
||||
|
||||
If our game has only non-consumables, we can
|
||||
|
||||
::
|
||||
|
||||
IAP.set_auto_consume(false)
|
||||
|
||||
We should set the auto consume value only once when the game starts.
|
||||
|
||||
Testing
|
||||
-------
|
||||
|
||||
If we add a gmail id as a tester in Google dashboard, that tester can purchase items and they will not be charged. Another way to test IAP is using redeem codes generated by us for our game because the purchase flow is the same.
|
||||
|
||||
Third way of testing is in development side. If we put the product ids as shown below, we will get a static fixed response according to the product id. This is a quick way of testing things before going to the dashboard.
|
||||
|
||||
- android.test.purchased
|
||||
- android.test.canceled
|
||||
- android.test.refunded
|
||||
- android.test.item_unavailable
|
||||
9
tutorials/platform/index.rst
Normal file
9
tutorials/platform/index.rst
Normal file
@@ -0,0 +1,9 @@
|
||||
Platform-specific
|
||||
=================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:name: toc-learn-features-platform
|
||||
|
||||
android_in_app_purchases
|
||||
services_for_ios
|
||||
427
tutorials/platform/services_for_ios.rst
Normal file
427
tutorials/platform/services_for_ios.rst
Normal file
@@ -0,0 +1,427 @@
|
||||
.. _doc_services_for_ios:
|
||||
|
||||
Services for iOS
|
||||
================
|
||||
|
||||
At the moment, there are 2 iOS APIs partially implemented, GameCenter
|
||||
and Storekit. Both use the same model of asynchronous calls explained
|
||||
below.
|
||||
|
||||
Asynchronous methods
|
||||
--------------------
|
||||
|
||||
When requesting an asynchronous operation, the method will look like
|
||||
this:
|
||||
|
||||
::
|
||||
|
||||
Error purchase(Variant p_params);
|
||||
|
||||
The parameter will usually be a Dictionary, with the information
|
||||
necessary to make the request, and the call will have 2 phases. First,
|
||||
the method will immediately return an Error value. If the Error is not
|
||||
'OK', the call operation is completed, with an error probably caused
|
||||
locally (no internet connection, API incorrectly configured, etc). If
|
||||
the error value is 'OK', a response event will be produced and added to
|
||||
the 'pending events' queue. Example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
func on_purchase_pressed():
|
||||
var result = InAppStore.purchase( { "product_id": "my_product" } )
|
||||
if result == OK:
|
||||
animation.play("busy") # show the "waiting for response" animation
|
||||
else:
|
||||
show_error()
|
||||
|
||||
# put this on a 1 second timer or something
|
||||
func check_events():
|
||||
while InAppStore.get_pending_event_count() > 0:
|
||||
var event = InAppStore.pop_pending_event()
|
||||
if event.type == "purchase":
|
||||
if event.result == "ok":
|
||||
show_success(event.product_id)
|
||||
else:
|
||||
show_error()
|
||||
|
||||
Remember that when a call returns OK, the API will *always* produce an
|
||||
event through the pending_event interface, even if it's an error, or a
|
||||
network timeout, etc. You should be able to, for example, safely block
|
||||
the interface waiting for a reply from the server. If any of the APIs
|
||||
don't behave this way it should be treated as a bug.
|
||||
|
||||
The pending event interface consists of 2 methods:
|
||||
|
||||
- ``get_pending_event_count()``
|
||||
Returns the number of pending events on the queue.
|
||||
|
||||
- ``Variant pop_pending_event()``
|
||||
Pops the first event from the queue and returns it.
|
||||
|
||||
Store Kit
|
||||
---------
|
||||
|
||||
Implemented in platform/iphone/in_app_store.mm
|
||||
|
||||
The Store Kit API is accessible through the "InAppStore" singleton (will
|
||||
always be available from gdscript). It is initialized automatically. It
|
||||
has 2 methods for purchasing:
|
||||
|
||||
- ``Error purchase(Variant p_params);``
|
||||
- ``Error request_product_info(Variant p_params);``
|
||||
|
||||
and the pending_event interface
|
||||
|
||||
::
|
||||
|
||||
int get_pending_event_count();
|
||||
Variant pop_pending_event();
|
||||
|
||||
purchase
|
||||
~~~~~~~~
|
||||
|
||||
Purchases a product id through the Store Kit API.
|
||||
|
||||
Parameters
|
||||
^^^^^^^^^^
|
||||
|
||||
Takes a Dictionary as a parameter, with one field, ``product_id``, a
|
||||
string with your product id. Example:
|
||||
|
||||
::
|
||||
|
||||
var result = InAppStore.purchase( { "product_id": "my_product" } )
|
||||
|
||||
Response event
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
The response event will be a dictionary with the following fields:
|
||||
|
||||
On error:
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"type": "purchase",
|
||||
"result": "error",
|
||||
"product_id": "the product id requested"
|
||||
}
|
||||
|
||||
On success:
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"type": "purchase",
|
||||
"result": "ok",
|
||||
"product_id": "the product id requested"
|
||||
}
|
||||
|
||||
request_product_info
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Requests the product info on a list of product IDs.
|
||||
|
||||
Parameters
|
||||
^^^^^^^^^^
|
||||
|
||||
Takes a Dictionary as a parameter, with one field, ``product_ids``, a
|
||||
string array with a list of product ids. Example:
|
||||
|
||||
::
|
||||
|
||||
var result = InAppStore.request_product_info( { "product_ids": ["my_product1", "my_product2"] } )
|
||||
|
||||
Response event
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
The response event will be a dictionary with the following fields:
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"type": "product_info",
|
||||
"result": "ok",
|
||||
"invalid_ids": [ list of requested ids that were invalid ],
|
||||
"ids": [ list of ids that were valid ],
|
||||
"titles": [ list of valid product titles (corresponds with list of valid ids) ],
|
||||
"descriptions": [ list of valid product descriptions ] ,
|
||||
"prices": [ list of valid product prices ],
|
||||
"localized_prices": [ list of valid product localized prices ],
|
||||
}
|
||||
|
||||
Game Center
|
||||
-----------
|
||||
|
||||
Implemented in platform/iphone/game_center.mm
|
||||
|
||||
The Game Center API is available through the "GameCenter" singleton. It
|
||||
has 6 methods:
|
||||
|
||||
- ``Error post_score(Variant p_score);``
|
||||
- ``Erroraward_achievement(Variant p_params);``
|
||||
- ``Error reset_achievements();``
|
||||
- ``Error request_achievements();``
|
||||
- ``Error request_achievement_descriptions();``
|
||||
- ``Error show_game_center(Variant p_params);``
|
||||
|
||||
plus the standard pending event interface.
|
||||
|
||||
post_score
|
||||
~~~~~~~~~~
|
||||
|
||||
Posts a score to a Game Center leaderboard.
|
||||
|
||||
Parameters
|
||||
^^^^^^^^^^
|
||||
|
||||
Takes a Dictionary as a parameter, with 2 fields:
|
||||
|
||||
- ``score`` a float number
|
||||
- ``category`` a string with the category name
|
||||
|
||||
Example:
|
||||
|
||||
::
|
||||
|
||||
var result = GameCenter.post_score( { "score": 100, "category": "my_leaderboard", } )
|
||||
|
||||
Response event
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
The response event will be a dictionary with the following fields:
|
||||
|
||||
On error:
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"type": "post_score",
|
||||
"result": "error",
|
||||
"error_code": the value from NSError::code,
|
||||
"error_description": the value from NSError::localizedDescription,
|
||||
}
|
||||
|
||||
On success:
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"type": "post_score",
|
||||
"result": "ok",
|
||||
}
|
||||
|
||||
award_achievement
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Modifies the progress of a Game Center achievement.
|
||||
|
||||
Parameters
|
||||
^^^^^^^^^^
|
||||
|
||||
Takes a Dictionary as a parameter, with 3 fields:
|
||||
|
||||
- ``name`` (string) the achievement name
|
||||
- ``progress`` (float) the achievement progress from 0.0 to 100.0
|
||||
(passed to ``GKAchievement::percentComplete``)
|
||||
- ``show_completion_banner`` (bool) whether Game Center should display
|
||||
an achievement banner at the top of the screen
|
||||
|
||||
Example:
|
||||
|
||||
::
|
||||
|
||||
var result = award_achievement( { "name": "hard_mode_completed", "progress": 6.1 } )
|
||||
|
||||
Response event
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
The response event will be a dictionary with the following fields:
|
||||
|
||||
On error:
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"type": "award_achievement",
|
||||
"result": "error",
|
||||
"error_code": the error code taken from NSError::code,
|
||||
}
|
||||
|
||||
On success:
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"type": "award_achievement",
|
||||
"result": "ok",
|
||||
}
|
||||
|
||||
reset_achievements
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Clears all Game Center achievements. The function takes no parameters.
|
||||
|
||||
Response event
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
The response event will be a dictionary with the following fields:
|
||||
|
||||
On error:
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"type": "reset_achievements",
|
||||
"result": "error",
|
||||
"error_code": the value from NSError::code
|
||||
}
|
||||
|
||||
On success:
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"type": "reset_achievements",
|
||||
"result": "ok",
|
||||
}
|
||||
|
||||
request_achievements
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Request all the Game Center achievements the player has made progress
|
||||
on. The function takes no parameters.
|
||||
|
||||
Response event
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
The response event will be a dictionary with the following fields:
|
||||
|
||||
On error:
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"type": "achievements",
|
||||
"result": "error",
|
||||
"error_code": the value from NSError::code
|
||||
}
|
||||
|
||||
On success:
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"type": "achievements",
|
||||
"result": "ok",
|
||||
"names": [ list of the name of each achievement ],
|
||||
"progress": [ list of the progress made on each achievement ]
|
||||
}
|
||||
|
||||
request_achievement_descriptions
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Request the descriptions of all existing Game Center achievements
|
||||
regardless of progress. The function takes no parameters.
|
||||
|
||||
Response event
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
The response event will be a dictionary with the following fields:
|
||||
|
||||
On error:
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"type": "achievement_descriptions",
|
||||
"result": "error",
|
||||
"error_code": the value from NSError::code
|
||||
}
|
||||
|
||||
On success:
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"type": "achievement_descriptions",
|
||||
"result": "ok",
|
||||
"names": [ list of the name of each achievement ],
|
||||
"titles": [ list of the title of each achievement ]
|
||||
"unachieved_descriptions": [ list of the description of each achievement when it is unachieved ]
|
||||
"achieved_descriptions": [ list of the description of each achievement when it is achieved ]
|
||||
"maximum_points": [ list of the points earned by completing each achievement ]
|
||||
"hidden": [ list of booleans indicating whether each achievement is initially visible ]
|
||||
"replayable": [ list of booleans indicating whether each achievement can be earned more than once ]
|
||||
}
|
||||
|
||||
show_game_center
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Displays the built in Game Center overlay showing leaderboards,
|
||||
achievements, and challenges.
|
||||
|
||||
Parameters
|
||||
^^^^^^^^^^
|
||||
|
||||
Takes a Dictionary as a parameter, with 2 fields:
|
||||
|
||||
- ``view`` (string) (optional) the name of the view to present. Accepts
|
||||
"default", "leaderboards", "achievements", or "challenges". Defaults
|
||||
to "default".
|
||||
- ``leaderboard_name`` (string) (optional) the name of the leaderboard
|
||||
to present. Only used when "view" is "leaderboards" (or "default" is
|
||||
configured to show leaderboards). If not specified, Game Center will
|
||||
display the aggregate leaderboard.
|
||||
|
||||
Examples:
|
||||
|
||||
::
|
||||
|
||||
var result = show_game_center( { "view": "leaderboards", "leaderboard_name": "best_time_leaderboard" } )
|
||||
var result = show_game_center( { "view": "achievements" } )
|
||||
|
||||
Response event
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
The response event will be a dictionary with the following fields:
|
||||
|
||||
On close:
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"type": "show_game_center",
|
||||
"result": "ok",
|
||||
}
|
||||
|
||||
Multi-platform games
|
||||
--------------------
|
||||
|
||||
When working on a multi-platform game, you won't always have the
|
||||
"GameCenter" singleton available (for example when running on PC or
|
||||
Android). Because the gdscript compiler looks up the singletons at
|
||||
compile time, you can't just query the singletons to see and use what
|
||||
you need inside a conditional block, you need to also define them as
|
||||
valid identifiers (local variable or class member). This is an example
|
||||
of how to work around this in a class:
|
||||
|
||||
.. code:: python
|
||||
|
||||
var GameCenter = null # define it as a class member
|
||||
|
||||
func post_score(p_score):
|
||||
if GameCenter == null:
|
||||
return
|
||||
GameCenter.post_score( { "value": p_score, "category": "my_leaderboard" } )
|
||||
|
||||
func check_events():
|
||||
while GameCenter.get_pending_event_count() > 0:
|
||||
# do something with events here
|
||||
pass
|
||||
|
||||
func _ready():
|
||||
# check if the singleton exists
|
||||
if Globals.has_singleton("GameCenter"):
|
||||
GameCenter = Globals.get_singleton("GameCenter")
|
||||
# connect your timer here to the "check_events" function
|
||||
Reference in New Issue
Block a user